qwerty-cli 0.0.1-alpha.10 → 0.0.1-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConfigEditor-GXFVIJP3.js → ConfigEditor-U4J6FVDN.js} +2 -2
- package/dist/{DictBrowser-SZVB5W25.js → DictBrowser-DZZWZXEA.js} +2 -2
- package/dist/{HelpScreen-OUP5G5UG.js → HelpScreen-7U5XF3IU.js} +2 -2
- package/dist/PracticeScreen-VLXPLVNW.js +2 -0
- package/dist/PracticeScreen-VLXPLVNW.js.map +1 -0
- package/dist/StatsViewer-PDXZ7PJR.js +2 -0
- package/dist/StatsViewer-PDXZ7PJR.js.map +1 -0
- package/dist/{WordLookup-DHZTVUR2.js → WordLookup-EAXT7PGW.js} +2 -2
- package/dist/chunk-IUFBN3RD.js +2 -0
- package/dist/{chunk-2MRNI465.js.map → chunk-IUFBN3RD.js.map} +1 -1
- package/dist/chunk-LN2WQT5M.js +3 -0
- package/dist/chunk-LN2WQT5M.js.map +1 -0
- package/dist/chunk-SSDQJ6MT.js +2 -0
- package/dist/chunk-SSDQJ6MT.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/menu.impl-EQABPGB2.js +2 -0
- package/dist/menu.impl-EQABPGB2.js.map +1 -0
- package/dist/practice.impl-NCY3HVPA.js +2 -0
- package/dist/{practice.impl-RAM2E7HR.js.map → practice.impl-NCY3HVPA.js.map} +1 -1
- package/package.json +1 -1
- package/dist/PracticeScreen-GBCDCFRM.js +0 -2
- package/dist/PracticeScreen-GBCDCFRM.js.map +0 -1
- package/dist/StatsViewer-IEHZS6GX.js +0 -2
- package/dist/StatsViewer-IEHZS6GX.js.map +0 -1
- package/dist/chunk-2MRNI465.js +0 -2
- package/dist/chunk-GUNCRBXR.js +0 -3
- package/dist/chunk-GUNCRBXR.js.map +0 -1
- package/dist/chunk-QEX27D7F.js +0 -2
- package/dist/chunk-QEX27D7F.js.map +0 -1
- package/dist/menu.impl-P6QJLLKA.js +0 -2
- package/dist/menu.impl-P6QJLLKA.js.map +0 -1
- package/dist/practice.impl-RAM2E7HR.js +0 -2
- /package/dist/{ConfigEditor-GXFVIJP3.js.map → ConfigEditor-U4J6FVDN.js.map} +0 -0
- /package/dist/{DictBrowser-SZVB5W25.js.map → DictBrowser-DZZWZXEA.js.map} +0 -0
- /package/dist/{HelpScreen-OUP5G5UG.js.map → HelpScreen-7U5XF3IU.js.map} +0 -0
- /package/dist/{WordLookup-DHZTVUR2.js.map → WordLookup-EAXT7PGW.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/nav.tsx","../src/i18n/context.tsx","../src/i18n/strings.ts","../src/i18n/locale.ts","../src/ui/components/BigWord.tsx"],"sourcesContent":["import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';\n\nexport type ScreenName =\n | 'main'\n | 'practice'\n | 'dict'\n | 'config'\n | 'stats'\n | 'word'\n | 'help';\n\nexport type PracticeParams = {\n dictId: string;\n chapterIndex: number;\n mode: 'order' | 'dictation' | 'review' | 'random' | 'loop';\n stealth?: boolean;\n};\n\nexport type DictParams = {\n pickerMode?: 'set-default' | 'choose-then-practice';\n};\n\nexport type ScreenFrame =\n | { name: 'main' }\n | { name: 'practice'; params: PracticeParams }\n | { name: 'dict'; params?: DictParams }\n | { name: 'config' }\n | { name: 'stats' }\n | { name: 'word' }\n | { name: 'help' };\n\ntype NavContextValue = {\n current: ScreenFrame;\n stack: ScreenFrame[];\n navigate: (frame: ScreenFrame) => void;\n replace: (frame: ScreenFrame) => void;\n back: () => void;\n reset: (frame: ScreenFrame) => void;\n};\n\nconst NavContext = createContext<NavContextValue | null>(null);\n\nexport function NavProvider({ initial, children }: { initial: ScreenFrame; children: ReactNode }) {\n const [stack, setStack] = useState<ScreenFrame[]>([initial]);\n\n const navigate = useCallback((frame: ScreenFrame) => {\n setStack((s) => [...s, frame]);\n }, []);\n const replace = useCallback((frame: ScreenFrame) => {\n setStack((s) => (s.length === 0 ? [frame] : [...s.slice(0, -1), frame]));\n }, []);\n const back = useCallback(() => {\n setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));\n }, []);\n const reset = useCallback((frame: ScreenFrame) => {\n setStack([frame]);\n }, []);\n\n const current = stack[stack.length - 1]!;\n return (\n <NavContext.Provider value={{ current, stack, navigate, replace, back, reset }}>\n {children}\n </NavContext.Provider>\n );\n}\n\nexport function useNav(): NavContextValue {\n const ctx = useContext(NavContext);\n if (!ctx) throw new Error('useNav must be used inside NavProvider');\n return ctx;\n}\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { en, zh, type Strings } from './strings.js';\nimport { detectLocale, type Lang, type LangPref } from './locale.js';\n\nconst StringsContext = createContext<{ lang: Lang; t: Strings } | null>(null);\n\nexport function StringsProvider({\n pref,\n children,\n}: {\n pref: LangPref;\n children: ReactNode;\n}) {\n const value = useMemo(() => {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n }, [pref]);\n return <StringsContext.Provider value={value}>{children}</StringsContext.Provider>;\n}\n\nexport function useStrings(): Strings {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useStrings must be used inside StringsProvider');\n return ctx.t;\n}\n\nexport function useLang(): Lang {\n const ctx = useContext(StringsContext);\n if (!ctx) throw new Error('useLang must be used inside StringsProvider');\n return ctx.lang;\n}\n\nexport function pickStrings(pref: LangPref): { lang: Lang; t: Strings } {\n const lang = detectLocale(pref);\n return { lang, t: lang === 'zh' ? zh : en };\n}\n","// Centralized string table for the qwerty-cli TUI.\n//\n// Print-only subcommands (qwerty config list, qwerty dict list, etc.) keep\n// English output regardless of locale so shell scripts stay predictable.\n// Only the interactive TUI is localized via this table.\n\nexport type Strings = {\n app: {\n title: string;\n subtitle: string;\n };\n common: {\n back: string;\n quit: string;\n on: string;\n off: string;\n cancel: string;\n };\n mainMenu: {\n items: {\n practiceLabel: string;\n practiceHintWith: (name: string) => string;\n practiceHintNone: string;\n dictLabel: string;\n dictHint: string;\n wordLabel: string;\n wordHint: string;\n statsLabel: string;\n statsHint: string;\n configLabel: string;\n configHint: string;\n stealthLabel: string;\n stealthHint: string;\n quitLabel: string;\n quitHint: string;\n };\n hint: string;\n helpHint: string;\n };\n dict: {\n title: string;\n loading: string;\n entries: (n: number) => string;\n filterPlaceholder: string;\n local: string;\n notLocal: string;\n defaultMark: string;\n tagsLabel: (tags: string) => string;\n wordsLabel: (n: number) => string;\n pulling: (id: string) => string;\n removing: (id: string) => string;\n errorOn: (id: string, msg: string) => string;\n footer: string;\n action: {\n title: string;\n setDefault: string;\n practice: string;\n delete: string;\n };\n command: {\n title: string;\n pull: string;\n import: string;\n refreshList: string;\n };\n };\n config: {\n title: string;\n fields: {\n defaultDict: string;\n defaultMode: string;\n accent: string;\n mirror: string;\n chapterSize: string;\n autoplayPronunciation: string;\n soundsMaster: string;\n soundsKeystroke: string;\n soundsFeedback: string;\n soundsKeySound: string;\n language: string;\n stealth: string;\n };\n enumValues: {\n stealth: { off: string; menu: string; default: string };\n };\n hints: {\n editing: string;\n bool: string;\n enum: string;\n dictRef: string;\n stringOrInt: string;\n };\n };\n stats: {\n title: string;\n loading: string;\n none: string;\n nonePractice: string;\n lifetime: string;\n sessions: string;\n words: string;\n errors: string;\n wpm: string;\n accuracy: string;\n streak: string;\n last: (n: number) => string;\n cycleWindow: string;\n recent: string;\n topMistakes: string;\n footer: string;\n maxLabel: string;\n recentUnits: { words: string; errors: string; wpm: string };\n multiDictSuffix: (n: number) => string;\n bars: { speed: string; accuracy: string; sessions: string };\n };\n word: {\n title: string;\n indexing: string;\n none: string;\n pullFirst: string;\n countAcross: (n: number) => string;\n noMatches: (q: string) => string;\n inDict: (name: string) => string;\n mistakes: (n: number, date: string) => string;\n footer: string;\n };\n practice: {\n loading: string;\n paused: string;\n chapterComplete: string;\n chapterLabel: (c: number, t: number) => string;\n reviewLabel: string;\n statusBar: {\n mode: string;\n accent: string;\n };\n modes: {\n order: string;\n dictation: string;\n review: string;\n random: string;\n loop: string;\n };\n accents: {\n us: string;\n uk: string;\n };\n statCards: {\n words: string;\n errors: string;\n wpm: string;\n accuracy: string;\n elapsed: (t: string) => string;\n };\n pause: {\n title: string;\n chapter: (c: number, t: number) => string;\n progress: (completed: number, total: number) => string;\n hint: string;\n };\n summary: {\n loopAgain: string;\n nextChapter: string;\n reviewMistakes: string;\n backMenu: string;\n };\n footers: {\n typing: string;\n };\n errors: {\n noMistakes: string;\n dictEmpty: (id: string) => string;\n unknown: string;\n };\n imeWarning: string;\n imeWarningShort: string;\n };\n audio: {\n noPlayer: string;\n };\n report: {\n title: string;\n duration: string;\n practiced: string;\n chapters: string;\n words: string;\n accuracy: string;\n wpm: string;\n newMistakes: string;\n farewell: string;\n notPracticed: string;\n };\n help: {\n title: string;\n subtitle: string;\n sections: {\n main: string;\n practice: string;\n dict: string;\n config: string;\n stats: string;\n word: string;\n global: string;\n };\n keys: {\n navigate: string;\n select: string;\n letterJump: string;\n pause: string;\n skip: string;\n replay: string;\n resume: string;\n backMenu: string;\n backScreen: string;\n nextChapter: string;\n reviewMistakes: string;\n filter: string;\n itemActions: string;\n moreActions: string;\n cycleWindow: string;\n stealthToggle: string;\n helpScreen: string;\n quit: string;\n };\n footer: string;\n };\n stealth: {\n paused: string;\n chapterDone: string;\n resumeHint: string;\n nextHint: string;\n pausedHintRight: string;\n nextHintRight: string;\n infoChipLabel: string;\n infoFmt: (\n dict: string,\n chapter: string,\n completed: number,\n total: number,\n wpm: number,\n accPct: number,\n ) => string;\n };\n};\n\nexport const en: Strings = {\n app: {\n title: 'qwerty',\n subtitle: 'typing practice for the terminal',\n },\n common: {\n back: 'back',\n quit: 'quit',\n on: 'on',\n off: 'off',\n cancel: 'cancel',\n },\n mainMenu: {\n items: {\n practiceLabel: 'Practice',\n practiceHintWith: (name) => `start ${name}`,\n practiceHintNone: 'pick a dictionary',\n dictLabel: 'Dictionaries',\n dictHint: 'browse, pull, set default',\n wordLabel: 'Word lookup',\n wordHint: 'search local dicts',\n statsLabel: 'Stats',\n statsHint: 'history & trends',\n configLabel: 'Config',\n configHint: 'edit preferences',\n stealthLabel: 'Stealth',\n stealthHint: 'quiet practice mode',\n quitLabel: 'Quit',\n quitHint: 'Esc or Ctrl+C also exits',\n },\n hint: '↑/↓ navigate · Enter select · letters jump',\n helpHint: '? help',\n },\n dict: {\n title: 'Dictionaries',\n loading: 'loading dictionaries…',\n entries: (n) => `${n} entries`,\n filterPlaceholder: 'type to filter',\n local: 'local ✓',\n notLocal: 'not local',\n defaultMark: 'default ★',\n tagsLabel: (tags) => `tags: ${tags}`,\n wordsLabel: (n) => `${n} words`,\n pulling: (id) => `pulling ${id}…`,\n removing: (id) => `removing ${id}…`,\n errorOn: (id, msg) => `error on ${id}: ${msg}`,\n footer: '↑/↓ select · Enter actions · Ctrl+K more · Esc back',\n action: {\n title: 'current dictionary',\n setDefault: 'set as default',\n practice: 'practice now',\n delete: 'delete local',\n },\n command: {\n title: 'more actions',\n pull: 'pull selected',\n import: 'import .json',\n refreshList: 'update dictionary list',\n },\n },\n config: {\n title: 'Config',\n fields: {\n defaultDict: 'default dict',\n defaultMode: 'default mode',\n accent: 'accent',\n mirror: 'dict mirror',\n chapterSize: 'chapter size',\n autoplayPronunciation: 'autoplay pronunciation',\n soundsMaster: 'sounds master',\n soundsKeystroke: 'sounds keystroke',\n soundsFeedback: 'sounds feedback',\n soundsKeySound: 'sounds key sound',\n language: 'language',\n stealth: 'stealth mode',\n },\n enumValues: {\n stealth: { off: 'off', menu: 'show in menu', default: 'default practice' },\n },\n hints: {\n editing: 'type to edit · Enter save · Esc cancel',\n bool: 'space toggle · ↑/↓ move · Esc back',\n enum: '←/→ cycle · ↑/↓ move · Esc back',\n dictRef: 'Enter pick dict · ↑/↓ move · Esc back',\n stringOrInt: 'Enter edit · ↑/↓ move · Esc back',\n },\n },\n stats: {\n title: 'Stats · overview',\n loading: 'loading stats…',\n none: 'No practice history yet.',\n nonePractice: 'Run a practice session first.',\n lifetime: 'lifetime',\n sessions: 'sessions',\n words: 'words',\n errors: 'errors',\n wpm: 'wpm',\n accuracy: 'accuracy',\n streak: 'streak',\n last: (n) => `last ${n} days (←/→ cycle window)`,\n cycleWindow: '←/→ cycle window · Esc back',\n recent: 'recent sessions',\n topMistakes: 'top mistakes',\n footer: '←/→ cycle window · Esc back',\n maxLabel: 'max',\n recentUnits: { words: 'w', errors: 'err', wpm: 'wpm' },\n multiDictSuffix: (n) => ` +${n} more`,\n bars: { speed: 'speed', accuracy: 'accuracy', sessions: 'sessions' },\n },\n word: {\n title: 'Word lookup',\n indexing: 'indexing local dictionaries…',\n none: 'No local dictionaries.',\n pullFirst: 'Pull one in Dictionaries first.',\n countAcross: (n) => `${n} words across local dicts`,\n noMatches: (q) => `no matches for \"${q}\"`,\n inDict: (name) => `in: ${name}`,\n mistakes: (n, date) => `mistakes: ${n} (last ${date})`,\n footer: 'type to filter · ↑/↓ select · Esc back',\n },\n practice: {\n loading: 'loading…',\n paused: 'PAUSED',\n chapterComplete: 'CHAPTER COMPLETE',\n chapterLabel: (c, t) => `chapter ${c}/${t}`,\n reviewLabel: 'review',\n statusBar: {\n mode: 'mode',\n accent: 'accent',\n },\n modes: {\n order: 'order',\n dictation: 'dictation',\n review: 'review',\n random: 'random',\n loop: 'loop',\n },\n accents: {\n us: 'us',\n uk: 'uk',\n },\n statCards: {\n words: 'words',\n errors: 'errors',\n wpm: 'wpm',\n accuracy: 'accuracy',\n elapsed: (t) => `elapsed ${t}`,\n },\n pause: {\n title: 'PAUSED',\n chapter: (c, t) => `chapter ${c}/${t}`,\n progress: (completed, total) => `${completed}/${total}`,\n hint: 'Enter resume · Esc back to menu',\n },\n summary: {\n loopAgain: 'again',\n nextChapter: 'next chapter',\n reviewMistakes: 'review mistakes',\n backMenu: 'back to menu',\n },\n footers: {\n typing: 'Ctrl+N skip · Esc pause · Tab replay',\n },\n errors: {\n noMistakes: 'No mistakes to review yet. Practice some chapters first.',\n dictEmpty: (id) => `Dictionary ${id} is empty.`,\n unknown: 'Unknown error',\n },\n imeWarning: '! Non-English input detected — switch your IME to English.',\n imeWarningShort: '! IME',\n },\n audio: {\n noPlayer: '! No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled.',\n },\n report: {\n title: 'Session summary',\n duration: 'duration',\n practiced: 'practiced',\n chapters: 'chapters',\n words: 'words',\n accuracy: 'accuracy',\n wpm: 'wpm',\n newMistakes: 'new mistakes',\n farewell: 'see you next time.',\n notPracticed: 'no practice this run',\n },\n help: {\n title: 'Help',\n subtitle: 'all shortcuts',\n sections: {\n main: 'main menu',\n practice: 'practice',\n dict: 'dictionaries',\n config: 'config',\n stats: 'stats',\n word: 'word lookup',\n global: 'global',\n },\n keys: {\n navigate: '↑/↓ navigate items',\n select: 'Enter confirm / continue',\n letterJump: 'letter jump to menu item',\n pause: 'Esc pause practice',\n skip: 'Ctrl+N skip current word (neutral)',\n replay: 'Tab replay pronunciation',\n resume: 'Enter resume from pause',\n backMenu: 'Esc back to previous screen',\n backScreen: 'Esc close panel or back',\n nextChapter: 'Enter next chapter',\n reviewMistakes: 'm review mistakes',\n filter: 'type to filter list',\n itemActions: 'Enter open actions panel',\n moreActions: 'Ctrl+K more actions panel',\n cycleWindow: '←/→ cycle day window',\n stealthToggle: 'Ctrl+I toggle stealth info row',\n helpScreen: '? open this help screen',\n quit: 'Ctrl+C quit immediately',\n },\n footer: 'Esc back',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter resume',\n nextHintRight: 'Enter next',\n infoChipLabel: 'info',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n\nexport const zh: Strings = {\n app: {\n title: 'qwerty',\n subtitle: '终端键盘练习',\n },\n common: {\n back: '返回',\n quit: '退出',\n on: '开',\n off: '关',\n cancel: '取消',\n },\n mainMenu: {\n items: {\n practiceLabel: '练习',\n practiceHintWith: (name) => `开始 ${name}`,\n practiceHintNone: '请先选词典',\n dictLabel: '词典',\n dictHint: '浏览、下载、设为默认',\n wordLabel: '查词',\n wordHint: '在本地词典中搜索',\n statsLabel: '统计',\n statsHint: '历史与趋势',\n configLabel: '设置',\n configHint: '修改偏好',\n stealthLabel: '神隐',\n stealthHint: '神隐练习模式',\n quitLabel: '退出',\n quitHint: 'Esc 或 Ctrl+C 退出',\n },\n hint: '↑/↓ 移动 · Enter 确认 · 字母直达',\n helpHint: '? 帮助',\n },\n dict: {\n title: '词典',\n loading: '加载词典中…',\n entries: (n) => `${n} 部词典`,\n filterPlaceholder: '输入过滤',\n local: '已下载 ✓',\n notLocal: '未下载',\n defaultMark: '默认 ★',\n tagsLabel: (tags) => `标签:${tags}`,\n wordsLabel: (n) => `${n} 词`,\n pulling: (id) => `拉取 ${id} 中…`,\n removing: (id) => `删除 ${id} 中…`,\n errorOn: (id, msg) => `${id} 出错:${msg}`,\n footer: '↑/↓ 选择 · Enter 操作 · Ctrl+K 更多 · Esc 返回',\n action: {\n title: '当前词典',\n setDefault: '设为默认',\n practice: '立即练习',\n delete: '删除本地',\n },\n command: {\n title: '更多功能',\n pull: '拉取选中',\n import: '导入 .json',\n refreshList: '更新词典列表',\n },\n },\n config: {\n title: '设置',\n fields: {\n defaultDict: '默认词典',\n defaultMode: '默认模式',\n accent: '发音',\n mirror: '词典镜像源',\n chapterSize: '章节单词数',\n autoplayPronunciation: '自动播放发音',\n soundsMaster: '音效总开关',\n soundsKeystroke: '按键音',\n soundsFeedback: '反馈音',\n soundsKeySound: '按键音色',\n language: '语言',\n stealth: '神隐模式',\n },\n enumValues: {\n stealth: { off: '关闭', menu: '主菜单显示', default: '默认练习模式' },\n },\n hints: {\n editing: '输入修改 · Enter 保存 · Esc 取消',\n bool: '空格切换 · ↑/↓ 移动 · Esc 返回',\n enum: '←/→ 切换 · ↑/↓ 移动 · Esc 返回',\n dictRef: 'Enter 选词典 · ↑/↓ 移动 · Esc 返回',\n stringOrInt: 'Enter 编辑 · ↑/↓ 移动 · Esc 返回',\n },\n },\n stats: {\n title: '统计 · 概览',\n loading: '加载统计中…',\n none: '还没有练习记录。',\n nonePractice: '先来一次练习吧。',\n lifetime: '累计',\n sessions: '会话',\n words: '词数',\n errors: '错误',\n wpm: '速度',\n accuracy: '准确率',\n streak: '连续天数',\n last: (n) => `最近 ${n} 天 (←/→ 切换窗口)`,\n cycleWindow: '←/→ 切换窗口 · Esc 返回',\n recent: '最近会话',\n topMistakes: '高频错词',\n footer: '←/→ 切换窗口 · Esc 返回',\n maxLabel: '最大',\n recentUnits: { words: '词', errors: '错', wpm: '速' },\n multiDictSuffix: (n) => ` 等 ${n} 部`,\n bars: { speed: '速度', accuracy: '准确率', sessions: '会话' },\n },\n word: {\n title: '查词',\n indexing: '索引本地词典中…',\n none: '没有本地词典。',\n pullFirst: '先在「词典」中拉取一部。',\n countAcross: (n) => `本地词典共 ${n} 词`,\n noMatches: (q) => `没有匹配「${q}」的词`,\n inDict: (name) => `来源:${name}`,\n mistakes: (n, date) => `错过 ${n} 次 (最近 ${date})`,\n footer: '输入过滤 · ↑/↓ 选择 · Esc 返回',\n },\n practice: {\n loading: '加载中…',\n paused: '已暂停',\n chapterComplete: '本章完成',\n chapterLabel: (c, t) => `第 ${c}/${t} 章`,\n reviewLabel: '复习',\n statusBar: {\n mode: '模式',\n accent: '发音',\n },\n modes: {\n order: '顺序',\n dictation: '默写',\n review: '复习',\n random: '乱序',\n loop: '循环',\n },\n accents: {\n us: '美',\n uk: '英',\n },\n statCards: {\n words: '词数',\n errors: '错误',\n wpm: '速度',\n accuracy: '准确率',\n elapsed: (t) => `耗时 ${t}`,\n },\n pause: {\n title: '已暂停',\n chapter: (c, t) => `第 ${c}/${t} 章`,\n progress: (completed, total) => `${completed}/${total}`,\n hint: 'Enter 继续 · Esc 返回菜单',\n },\n summary: {\n loopAgain: '再来一遍',\n nextChapter: '下一章',\n reviewMistakes: '复习错词',\n backMenu: '返回菜单',\n },\n footers: {\n typing: 'Ctrl+N 跳过 · Esc 暂停 · Tab 重播',\n },\n errors: {\n noMistakes: '错词本是空的。先练习几章吧。',\n dictEmpty: (id) => `词典 ${id} 是空的。`,\n unknown: '未知错误',\n },\n imeWarning: '! 检测到中文/日文/韩文输入,请切换到英文输入法',\n imeWarningShort: '! 中文输入',\n },\n audio: {\n noPlayer: '! 未在 PATH 中找到音频播放器(尝试 afplay/ffplay/mpg123/paplay/aplay/powershell)。音效已禁用。',\n },\n report: {\n title: '本次会话',\n duration: '总时长',\n practiced: '练习用时',\n chapters: '完成章节',\n words: '词数',\n accuracy: '准确率',\n wpm: '速度',\n newMistakes: '新错词',\n farewell: '下次见。',\n notPracticed: '本次未练习',\n },\n help: {\n title: '帮助',\n subtitle: '全部快捷键',\n sections: {\n main: '主菜单',\n practice: '练习',\n dict: '词典',\n config: '设置',\n stats: '统计',\n word: '查词',\n global: '全局',\n },\n keys: {\n navigate: '↑/↓ 移动选项',\n select: 'Enter 确认 / 继续',\n letterJump: '字母键 直达菜单项',\n pause: 'Esc 暂停练习',\n skip: 'Ctrl+N 跳过当前词(不计错)',\n replay: 'Tab 重播发音',\n resume: 'Enter 继续练习',\n backMenu: 'Esc 返回上一屏',\n backScreen: 'Esc 关闭面板 / 返回',\n nextChapter: 'Enter 下一章',\n reviewMistakes: 'm 复习错词',\n filter: '输入 过滤列表',\n itemActions: 'Enter 弹出动作面板',\n moreActions: 'Ctrl+K 弹出更多功能',\n cycleWindow: '←/→ 切换日窗口',\n stealthToggle: 'Ctrl+I 切换神隐信息行',\n helpScreen: '? 打开本帮助页',\n quit: 'Ctrl+C 立即退出',\n },\n footer: 'Esc 返回',\n },\n stealth: {\n paused: 'paused',\n chapterDone: 'chapter done',\n resumeHint: 'Enter resume · Esc menu',\n nextHint: 'Enter next · Esc menu',\n pausedHintRight: 'Enter 继续',\n nextHintRight: 'Enter 下一章',\n infoChipLabel: '信息',\n infoFmt: (dict, chapter, completed, total, wpm, accPct) =>\n `${dict} · ${chapter} · ${completed}/${total} · ${wpm} wpm · ${accPct}%`,\n },\n};\n","export type Lang = 'zh' | 'en';\nexport type LangPref = 'auto' | Lang;\n\nfunction pickFromString(s: string | undefined): Lang | null {\n if (!s) return null;\n const lower = s.toLowerCase();\n if (lower.startsWith('zh')) return 'zh';\n if (lower.startsWith('en')) return 'en';\n return null;\n}\n\nexport function detectLocale(pref: LangPref): Lang {\n if (pref === 'zh' || pref === 'en') return pref;\n const env =\n process.env.LC_ALL ||\n process.env.LC_MESSAGES ||\n process.env.LANG ||\n process.env.LANGUAGE;\n const fromEnv = pickFromString(env);\n if (fromEnv) return fromEnv;\n try {\n const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;\n const fromIntl = pickFromString(intlLocale);\n if (fromIntl) return fromIntl;\n } catch {\n // ignore\n }\n return 'en';\n}\n","import { Box, Text } from 'ink';\n\nexport const PALETTE = {\n accent: '#5eead4',\n muted: '#6b7280',\n text: '#e5e7eb',\n primary: '#7dcfff',\n success: '#86efac',\n warning: '#fbbf24',\n error: '#f87171',\n} as const;\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n};\n\nexport function BigWord({ target, typed, error = false, hideTarget = false }: Props) {\n const chars = [...target];\n const typedChars = [...typed];\n\n return (\n <Box paddingY={4} justifyContent=\"center\">\n {chars.map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error\n ? PALETTE.error\n : isTyped\n ? PALETTE.accent\n : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n </Box>\n );\n}\n"],"mappings":"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","names":["createContext","useContext","useState","useCallback","jsx","NavContext","NavProvider","initial","children","stack","setStack","navigate","frame","s","replace","back","reset","current","useNav","ctx","createContext","useContext","useMemo","en","name","n","tags","id","msg","q","date","c","t","completed","total","dict","chapter","wpm","accPct","zh","pickFromString","s","lower","detectLocale","pref","env","fromEnv","intlLocale","fromIntl","jsx","StringsContext","createContext","StringsProvider","pref","children","value","useMemo","lang","detectLocale","zh","en","useStrings","ctx","useContext","pickStrings","pref","lang","detectLocale","zh","en","Box","Text","jsx","PALETTE","BigWord","target","typed","error","hideTarget","chars","typedChars","ch","i","isTyped","display","color"]}
|
package/dist/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Command as h}from"commander";import{Command as l}from"commander";function r(){let t=new l("config").description("Manage CLI configuration");return t.command("list").description("Show the effective merged config").action(async()=>{let{configList:o}=await import("./config.impl-IYJ4ZUPE.js");await o()}),t.command("get <key>").description("Get a config value by dotted path (e.g. sounds.keystroke)").action(async o=>{let{configGet:a}=await import("./config.impl-IYJ4ZUPE.js");await a(o)}),t.command("set <key> <value>").description("Set a config value by dotted path").action(async(o,a)=>{let{configSet:n}=await import("./config.impl-IYJ4ZUPE.js");await n(o,a)}),t}import{Command as u}from"commander";function e(){let t=new u("dict").description("Manage dictionaries");return t.command("list").description("List dictionaries (\u2713 = locally available)").option("-c, --category <category>","filter by category").option("--local-only","show only locally downloaded dictionaries").action(async o=>{let{dictList:a}=await import("./dict.impl-Y66SRRZL.js");await a(o)}),t.command("search <keyword>").description("Search the upstream registry by name/description/category/tags").option("-c, --category <category>","restrict to category").option("-l, --language <language>","restrict to language").action(async(o,a)=>{let{dictSearch:n}=await import("./dict.impl-Y66SRRZL.js");await n(o,a)}),t.command("pull <id>").description("Download an upstream dictionary into the local cache").action(async o=>{let{dictPull:a}=await import("./dict.impl-Y66SRRZL.js");await a(o)}),t.command("import <file>").description("Import a local qwerty-native JSON dictionary").requiredOption("--id <id>","dictionary id (lowercase, digits, dashes)").action(async(o,a)=>{let{dictImport:n}=await import("./dict.impl-Y66SRRZL.js");await n(o,a)}),t.command("rm <id>").description("Remove a local dictionary").action(async o=>{let{dictRemove:a}=await import("./dict.impl-Y66SRRZL.js");await a(o)}),t}import{Command as g}from"commander";function c(){return new g("practice").argument("[dictId]","dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").option("--stealth","enter stealth mode (minimal UI, no sound)").action(async(t,o)=>{let{runPractice:a}=await import("./practice.impl-
|
|
1
|
+
import{Command as h}from"commander";import{Command as l}from"commander";function r(){let t=new l("config").description("Manage CLI configuration");return t.command("list").description("Show the effective merged config").action(async()=>{let{configList:o}=await import("./config.impl-IYJ4ZUPE.js");await o()}),t.command("get <key>").description("Get a config value by dotted path (e.g. sounds.keystroke)").action(async o=>{let{configGet:a}=await import("./config.impl-IYJ4ZUPE.js");await a(o)}),t.command("set <key> <value>").description("Set a config value by dotted path").action(async(o,a)=>{let{configSet:n}=await import("./config.impl-IYJ4ZUPE.js");await n(o,a)}),t}import{Command as u}from"commander";function e(){let t=new u("dict").description("Manage dictionaries");return t.command("list").description("List dictionaries (\u2713 = locally available)").option("-c, --category <category>","filter by category").option("--local-only","show only locally downloaded dictionaries").action(async o=>{let{dictList:a}=await import("./dict.impl-Y66SRRZL.js");await a(o)}),t.command("search <keyword>").description("Search the upstream registry by name/description/category/tags").option("-c, --category <category>","restrict to category").option("-l, --language <language>","restrict to language").action(async(o,a)=>{let{dictSearch:n}=await import("./dict.impl-Y66SRRZL.js");await n(o,a)}),t.command("pull <id>").description("Download an upstream dictionary into the local cache").action(async o=>{let{dictPull:a}=await import("./dict.impl-Y66SRRZL.js");await a(o)}),t.command("import <file>").description("Import a local qwerty-native JSON dictionary").requiredOption("--id <id>","dictionary id (lowercase, digits, dashes)").action(async(o,a)=>{let{dictImport:n}=await import("./dict.impl-Y66SRRZL.js");await n(o,a)}),t.command("rm <id>").description("Remove a local dictionary").action(async o=>{let{dictRemove:a}=await import("./dict.impl-Y66SRRZL.js");await a(o)}),t}import{Command as g}from"commander";function c(){return new g("practice").argument("[dictId]","dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").option("--stealth","enter stealth mode (minimal UI, no sound)").action(async(t,o)=>{let{runPractice:a}=await import("./practice.impl-NCY3HVPA.js");await a(t,o)})}import{Command as y}from"commander";function d(){return new y("stats").description("Show practice history and trends").option("-d, --days <n>","window size for trend (default 14)","14").option("--top <n>","how many top mistakes to show (default 10)","10").action(async t=>{let{runStats:o}=await import("./stats.impl-IXVF3Q5Y.js");await o(t)})}import{Command as w}from"commander";function m(){return new w("word").argument("<keyword>").description("Look up a word across local dictionaries").option("--exact","require exact (case-insensitive) match").action(async(t,o)=>{let{runWordLookup:a}=await import("./word.impl-C4AYZ3NC.js");await a(t,o)})}import{Command as f}from"commander";function s(){return new f("boss").alias("stealth").description("Start practice in stealth mode (minimal UI, looks like plain terminal output)").argument("[dictId]","dictionary id; falls back to config.defaultDict").option("-c, --chapter <n>","chapter number (1-based)","1").option("-m, --mode <mode>","order | dictation | review | random | loop").action(async(t,o)=>{let{runPractice:a}=await import("./practice.impl-NCY3HVPA.js");await a(t,{...o,stealth:!0})})}async function p(){if(!process.stdout.isTTY){console.log("qwerty-cli \u2014 run `qwerty --help` for available commands.");return}let{runMainMenuImpl:t}=await import("./menu.impl-EQABPGB2.js");await t()}var i=new h;i.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version("0.0.1-alpha.11");i.addCommand(c());i.addCommand(s());i.addCommand(e());i.addCommand(m());i.addCommand(d());i.addCommand(r());i.action(async()=>{await p()});i.parseAsync(process.argv).catch(t=>{console.error(t instanceof Error?t.message:t),process.exit(1)});
|
|
2
2
|
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as o,b as n,c as s,d as p,e as c}from"./chunk-LN2WQT5M.js";import{h as a,j as m}from"./chunk-WE6IV5XB.js";import"./chunk-2GTGXODM.js";import{a as i}from"./chunk-ELWVQGDK.js";import"./chunk-IUFBN3RD.js";import{e}from"./chunk-SSDQJ6MT.js";import"./chunk-6QICLHIY.js";import"./chunk-6KRVNT2S.js";import{render as g}from"ink";import{createElement as h}from"react";async function y(){o();let r=await i();a();let{waitUntilExit:f}=g(h(s,{initial:{name:"main"},initialCfg:r}),{patchConsole:!1,exitOnCtrlC:!1});await f(),p();let t=n();if(t?.type==="stealth"){let{runPractice:u}=await import("./practice.impl-NCY3HVPA.js");await u(t.dictId,{chapter:t.chapterIndex+1,mode:t.mode,stealth:!0});return}let{lang:l,t:d}=e(r.language);c(m(),d,l)}export{y as runMainMenuImpl};
|
|
2
|
+
//# sourceMappingURL=menu.impl-EQABPGB2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/menu.impl.ts"],"sourcesContent":["import { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport { consumePostExitAction } from '../util/post-exit-action.js';\n\nexport async function runMainMenuImpl(): Promise<void> {\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, the menu leaks under\n // the exit report when the user quits.\n enterAltScreen();\n\n const cfg = await loadConfig();\n startSession();\n const { waitUntilExit } = render(\n createElement(App, { initial: { name: 'main' }, initialCfg: cfg }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n ensureMainScreen();\n\n // Handoff: if the menu requested stealth practice, leave alt-screen (done)\n // and re-enter via runPractice() in inline mode so the 3 rows render under\n // the real shell scrollback. runPractice prints its own report at the end.\n const pending = consumePostExitAction();\n if (pending?.type === 'stealth') {\n const { runPractice } = await import('./practice.impl.js');\n await runPractice(pending.dictId, {\n chapter: pending.chapterIndex + 1,\n mode: pending.mode,\n stealth: true,\n });\n return;\n }\n\n const { lang, t } = pickStrings(cfg.language);\n printSessionReport(sessionReport(), t, lang);\n}\n"],"mappings":"8SAAA,OAAS,UAAAA,MAAc,MACvB,OAAS,iBAAAC,MAAqB,QAS9B,eAAsBC,GAAiC,CAIrDC,EAAe,EAEf,IAAMC,EAAM,MAAMC,EAAW,EAC7BC,EAAa,EACb,GAAM,CAAE,cAAAC,CAAc,EAAIC,EACxBC,EAAcC,EAAK,CAAE,QAAS,CAAE,KAAM,MAAO,EAAG,WAAYN,CAAI,CAAC,EACjE,CAAE,aAAc,GAAO,YAAa,EAAM,CAC5C,EACA,MAAMG,EAAc,EACpBI,EAAiB,EAKjB,IAAMC,EAAUC,EAAsB,EACtC,GAAID,GAAS,OAAS,UAAW,CAC/B,GAAM,CAAE,YAAAE,CAAY,EAAI,KAAM,QAAO,6BAAoB,EACzD,MAAMA,EAAYF,EAAQ,OAAQ,CAChC,QAASA,EAAQ,aAAe,EAChC,KAAMA,EAAQ,KACd,QAAS,EACX,CAAC,EACD,MACF,CAEA,GAAM,CAAE,KAAAG,EAAM,EAAAC,CAAE,EAAIC,EAAYb,EAAI,QAAQ,EAC5Cc,EAAmBC,EAAc,EAAGH,EAAGD,CAAI,CAC7C","names":["render","createElement","runMainMenuImpl","enterAltScreen","cfg","loadConfig","start","waitUntilExit","render","createElement","App","ensureMainScreen","pending","consumePostExitAction","runPractice","lang","t","pickStrings","printSessionReport","report"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as c,c as p,d as f,e as u}from"./chunk-LN2WQT5M.js";import{h as m,j as l}from"./chunk-WE6IV5XB.js";import"./chunk-2GTGXODM.js";import{a}from"./chunk-ELWVQGDK.js";import"./chunk-IUFBN3RD.js";import{e as d}from"./chunk-SSDQJ6MT.js";import"./chunk-6QICLHIY.js";import"./chunk-6KRVNT2S.js";import n from"chalk";import{render as S}from"ink";import{createElement as v}from"react";var g=["order","dictation","review","random","loop"];function w(t){return g.includes(t)}async function A(t,o){if(!process.stdout.isTTY){console.error(n.red("Practice requires an interactive TTY.")),process.exitCode=1;return}let e=await a(),s=t??e.defaultDict;if(!s){console.error(n.red("No dictionary specified. Pass an id or set config.defaultDict.")),process.exitCode=1;return}let i=o.mode??e.defaultMode;if(!w(i)){console.error(n.red(`Invalid mode "${i}". Valid: ${g.join(", ")}`)),process.exitCode=1;return}let h=Math.max(0,Number(o.chapter??1)-1),r=o.stealth===!0||e.stealth==="default";r||c(),m();let{waitUntilExit:x}=S(v(p,{initial:{name:"practice",params:{dictId:s,chapterIndex:h,mode:i,stealth:r}},initialCfg:e,inline:r}),{patchConsole:!1,exitOnCtrlC:!1});await x(),r||f();let{lang:C,t:M}=d(e.language);u(l(),M,C)}export{A as runPractice};
|
|
2
|
+
//# sourceMappingURL=practice.impl-NCY3HVPA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/practice.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport type { Mode } from '../domain/chapters.js';\n\nconst MODES: Mode[] = ['order', 'dictation', 'review', 'random', 'loop'];\n\nfunction isMode(v: string): v is Mode {\n return (MODES as string[]).includes(v);\n}\n\nexport async function runPractice(\n dictIdArg: string | undefined,\n options: { chapter?: string | number; mode?: string; stealth?: boolean },\n): Promise<void> {\n if (!process.stdout.isTTY) {\n console.error(chalk.red('Practice requires an interactive TTY.'));\n process.exitCode = 1;\n return;\n }\n const cfg = await loadConfig();\n const dictId = dictIdArg ?? cfg.defaultDict;\n if (!dictId) {\n console.error(chalk.red('No dictionary specified. Pass an id or set config.defaultDict.'));\n process.exitCode = 1;\n return;\n }\n const mode = options.mode ?? cfg.defaultMode;\n if (!isMode(mode)) {\n console.error(chalk.red(`Invalid mode \"${mode}\". Valid: ${MODES.join(', ')}`));\n process.exitCode = 1;\n return;\n }\n const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);\n const stealth = options.stealth === true || cfg.stealth === 'default';\n\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, when the user exits,\n // the main screen still has the menu/practice content from frame 0\n // showing under the exit report. Stealth uses inline mode, skip.\n if (!stealth) enterAltScreen();\n\n startSession();\n const { waitUntilExit } = render(\n createElement(App, {\n initial: { name: 'practice', params: { dictId, chapterIndex, mode, stealth } },\n initialCfg: cfg,\n inline: stealth,\n }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n if (stealth
|
|
1
|
+
{"version":3,"sources":["../src/commands/practice.impl.ts"],"sourcesContent":["import chalk from 'chalk';\nimport { render } from 'ink';\nimport { createElement } from 'react';\nimport { loadConfig } from '../infra/config-store.js';\nimport { App } from '../ui/App.js';\nimport { start as startSession, report as sessionReport } from '../infra/session-tracker.js';\nimport { printSessionReport, ensureMainScreen } from '../util/report.js';\nimport { pickStrings } from '../i18n/context.js';\nimport { enterAltScreen } from '../util/altscreen.js';\nimport type { Mode } from '../domain/chapters.js';\n\nconst MODES: Mode[] = ['order', 'dictation', 'review', 'random', 'loop'];\n\nfunction isMode(v: string): v is Mode {\n return (MODES as string[]).includes(v);\n}\n\nexport async function runPractice(\n dictIdArg: string | undefined,\n options: { chapter?: string | number; mode?: string; stealth?: boolean },\n): Promise<void> {\n if (!process.stdout.isTTY) {\n console.error(chalk.red('Practice requires an interactive TTY.'));\n process.exitCode = 1;\n return;\n }\n const cfg = await loadConfig();\n const dictId = dictIdArg ?? cfg.defaultDict;\n if (!dictId) {\n console.error(chalk.red('No dictionary specified. Pass an id or set config.defaultDict.'));\n process.exitCode = 1;\n return;\n }\n const mode = options.mode ?? cfg.defaultMode;\n if (!isMode(mode)) {\n console.error(chalk.red(`Invalid mode \"${mode}\". Valid: ${MODES.join(', ')}`));\n process.exitCode = 1;\n return;\n }\n const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);\n const stealth = options.stealth === true || cfg.stealth === 'default';\n\n // Enter alt-screen synchronously BEFORE render() so Ink's first frame\n // never lands on the main screen. Without this, when the user exits,\n // the main screen still has the menu/practice content from frame 0\n // showing under the exit report. Stealth uses inline mode, skip.\n if (!stealth) enterAltScreen();\n\n startSession();\n const { waitUntilExit } = render(\n createElement(App, {\n initial: { name: 'practice', params: { dictId, chapterIndex, mode, stealth } },\n initialCfg: cfg,\n inline: stealth,\n }),\n { patchConsole: false, exitOnCtrlC: false },\n );\n await waitUntilExit();\n if (!stealth) {\n ensureMainScreen();\n }\n // Stealth: leave the 3 inline rows in scrollback so the practice \"stacks\"\n // under the previous shell session — that's the whole point of the mode.\n const { lang, t } = pickStrings(cfg.language);\n printSessionReport(sessionReport(), t, lang);\n}\n"],"mappings":"uSAAA,OAAOA,MAAW,QAClB,OAAS,UAAAC,MAAc,MACvB,OAAS,iBAAAC,MAAqB,QAS9B,IAAMC,EAAgB,CAAC,QAAS,YAAa,SAAU,SAAU,MAAM,EAEvE,SAASC,EAAOC,EAAsB,CACpC,OAAQF,EAAmB,SAASE,CAAC,CACvC,CAEA,eAAsBC,EACpBC,EACAC,EACe,CACf,GAAI,CAAC,QAAQ,OAAO,MAAO,CACzB,QAAQ,MAAMC,EAAM,IAAI,uCAAuC,CAAC,EAChE,QAAQ,SAAW,EACnB,MACF,CACA,IAAMC,EAAM,MAAMC,EAAW,EACvBC,EAASL,GAAaG,EAAI,YAChC,GAAI,CAACE,EAAQ,CACX,QAAQ,MAAMH,EAAM,IAAI,gEAAgE,CAAC,EACzF,QAAQ,SAAW,EACnB,MACF,CACA,IAAMI,EAAOL,EAAQ,MAAQE,EAAI,YACjC,GAAI,CAACN,EAAOS,CAAI,EAAG,CACjB,QAAQ,MAAMJ,EAAM,IAAI,iBAAiBI,CAAI,aAAaV,EAAM,KAAK,IAAI,CAAC,EAAE,CAAC,EAC7E,QAAQ,SAAW,EACnB,MACF,CACA,IAAMW,EAAe,KAAK,IAAI,EAAG,OAAON,EAAQ,SAAW,CAAC,EAAI,CAAC,EAC3DO,EAAUP,EAAQ,UAAY,IAAQE,EAAI,UAAY,UAMvDK,GAASC,EAAe,EAE7BC,EAAa,EACb,GAAM,CAAE,cAAAC,CAAc,EAAIC,EACxBC,EAAcC,EAAK,CACjB,QAAS,CAAE,KAAM,WAAY,OAAQ,CAAE,OAAAT,EAAQ,aAAAE,EAAc,KAAAD,EAAM,QAAAE,CAAQ,CAAE,EAC7E,WAAYL,EACZ,OAAQK,CACV,CAAC,EACD,CAAE,aAAc,GAAO,YAAa,EAAM,CAC5C,EACA,MAAMG,EAAc,EACfH,GACHO,EAAiB,EAInB,GAAM,CAAE,KAAAC,EAAM,EAAAC,CAAE,EAAIC,EAAYf,EAAI,QAAQ,EAC5CgB,EAAmBC,EAAc,EAAGH,EAAGD,CAAI,CAC7C","names":["chalk","render","createElement","MODES","isMode","v","runPractice","dictIdArg","options","chalk","cfg","loadConfig","dictId","mode","chapterIndex","stealth","enterAltScreen","start","waitUntilExit","render","createElement","App","ensureMainScreen","lang","t","pickStrings","printSessionReport","report"]}
|
package/package.json
CHANGED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{a as ct,b as it,c as st,d as at,e as ut,f as lt,i as gt}from"./chunk-WE6IV5XB.js";import{b as Y}from"./chunk-2GTGXODM.js";import{a as ht}from"./chunk-MPE25TTQ.js";import{e as ot}from"./chunk-TP77EGJ2.js";import"./chunk-ELWVQGDK.js";import{a as V,b as ft,c as pt}from"./chunk-UPA4JFCH.js";import{b as dt,d as D}from"./chunk-2MRNI465.js";import{b as X,d as M,f as s,g as mt}from"./chunk-QEX27D7F.js";import"./chunk-6QICLHIY.js";import"./chunk-6KRVNT2S.js";import{useState as L,useEffect as H,useRef as Z}from"react";import{Box as m,Text as g,useApp as ne,useInput as z}from"ink";function xt(t,e=Math.random){let r=[...t];for(let n=r.length-1;n>0;n--){let c=Math.floor(e()*(n+1)),i=r[n];r[n]=r[c],r[c]=i}return r}function bt(t){let e=t>>>0;return()=>{e=e+1831565813>>>0;let r=Math.imul(e^e>>>15,1|e);return r=r+Math.imul(r^r>>>7,61|r)^r,((r^r>>>14)>>>0)/4294967296}}function yt(t,e){if(e<=0)throw new Error("chapterSize must be positive");let r=[];for(let n=0;n<t.length;n+=e)r.push(t.slice(n,n+e));return r}function wt(t,e,r){if(e==="random"){let n=r===void 0?Math.random:bt(r);return xt(t,n)}return t}function _(t){return{target:t,typed:"",errorsThisWord:0}}function Tt(t,e){switch(e.type){case"reset":return{state:{...t,typed:""},effect:"none"};case"backspace":return t.typed.length===0?{state:t,effect:"none"}:{state:{...t,typed:t.typed.slice(0,-1)},effect:"none"};case"char":{let r=t.typed+e.ch,n=[...t.target].slice(0,[...r].length).join("");return r===n?r.length===t.target.length?{state:{...t,typed:r},effect:"correct"}:{state:{...t,typed:r},effect:"progress"}:{state:{...t,typed:"",errorsThisWord:t.errorsThisWord+1},effect:"wrong"}}}}function q(t,e=Date.now()){return t.length===0?{startedAt:e,results:[],current:null,finishedAt:e,playlist:t}:{startedAt:e,results:[],current:{wordIndex:0,wordStartedAt:e,input:_(t[0].name)},finishedAt:null,playlist:t}}function J(t,e,r=Date.now()){if(!t.current)return{session:t,effect:"none"};let{state:n,effect:c}=Tt(t.current.input,e);if(c==="correct"){let i={word:n.target,errors:n.errorsThisWord,durationMs:r-t.current.wordStartedAt},u=t.current.wordIndex+1,a=[...t.results,i];return u>=t.playlist.length?{session:{...t,results:a,current:null,finishedAt:r},effect:c}:{session:{...t,results:a,current:{wordIndex:u,wordStartedAt:r,input:_(t.playlist[u].name)}},effect:c}}return{session:{...t,current:{...t.current,input:n}},effect:c}}function It(t,e=Date.now()){if(!t.current)return{session:t,effect:"none"};let r={word:t.current.input.target,errors:0,durationMs:e-t.current.wordStartedAt,skipped:!0},n=t.current.wordIndex+1,c=[...t.results,r];return n>=t.playlist.length?{session:{...t,results:c,current:null,finishedAt:e},effect:"skipped"}:{session:{...t,results:c,current:{wordIndex:n,wordStartedAt:e,input:_(t.playlist[n].name)}},effect:"skipped"}}function Q(t){let e=t.results.reduce((c,i)=>c+i.errors,0),r=(t.finishedAt??Date.now())-t.startedAt,n={};for(let c of t.results)c.errors>0&&(n[c.word]=(n[c.word]??0)+c.errors);return{wordCount:t.results.length,errors:e,durationMs:r,perWordErrors:n}}import{useEffect as Ct,useReducer as zt,useRef as Ut,useState as Kt}from"react";import{useInput as Xt,useApp as Yt}from"ink";function qt(t,e){if(e.type==="start")return{session:q(e.playlist,e.now),lastEffect:null};if(e.type==="skip"){let r=It(t.session,e.now);return{session:r.session,lastEffect:r.effect}}if(e.type==="event"){if(e.key.backspace||e.key.delete){let c=J(t.session,{type:"backspace"},e.now);return{session:c.session,lastEffect:c.effect}}if(e.input.length===0)return t;let r=t.session,n=t.lastEffect;for(let c of e.input){let i=J(r,{type:"char",ch:c},e.now);if(r=i.session,n=i.effect,r.finishedAt!==null)break}return{session:r,lastEffect:n}}return t}function St({playlist:t,onComplete:e,onTab:r,onEscape:n,onSkip:c,enabled:i=!0}){let[u,a]=zt(qt,void 0,()=>({session:q(t,Date.now()),lastEffect:null})),f=Ut(!1),[y,k]=Kt(0),{exit:T}=Yt();return Xt((b,d)=>{if(d.ctrl&&b==="c"){T();return}if(d.ctrl&&b==="n"){c?.(),a({type:"skip",now:Date.now()});return}if(d.escape){n?.();return}if(d.tab){r?.();return}if(d.upArrow||d.downArrow||d.leftArrow||d.rightArrow||d.return||d.ctrl||d.meta)return;let x=[...b].filter(N=>{let W=N.codePointAt(0);return W===32||W>=33&&W!==127}).join("");x.length!==0&&a({type:"event",input:x,key:d,now:Date.now()})},{isActive:i}),Ct(()=>{u.session.finishedAt!==null&&!f.current&&(f.current=!0,e(u.session))},[u.session,e]),Ct(()=>{if(u.session.finishedAt!==null)return;let b=setInterval(()=>k(d=>d+1),1e3);return()=>clearInterval(b)},[u.session.finishedAt]),{session:u.session,lastEffect:u.lastEffect,tick:y}}import{useEffect as Jt,useRef as Qt}from"react";function Mt(t){let e=Qt(!1);return Jt(()=>{e.current||(e.current=!0,ct(!t.enabled).catch(()=>{}))},[t.enabled]),{keystroke:()=>t.enabled&&it(),correct:()=>t.enabled&&st(),wrong:()=>t.enabled&&at(),pronounce:r=>{t.enabled&&t.autoplayPronunciation&&ut(r,t.accent)},prefetch:r=>{t.enabled&<(r,t.accent)}}}import{useCallback as Zt}from"react";function kt(t){return Zt(async e=>{let r={ts:new Date().toISOString(),dictId:t.dictId,chapter:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors};await ht(r),gt({dictId:t.dictId,chapterIndex:t.chapterIndex,mode:t.mode,wordCount:e.wordCount,errors:e.errors,durationMs:e.durationMs,perWordErrors:e.perWordErrors});let n=Object.entries(e.perWordErrors).filter(([,i])=>i>0);if(n.length===0)return;let c=await V();for(let[i,u]of n)c=pt(c,i,t.dictId,u);await ft(c)},[t.dictId,t.chapterIndex,t.mode])}import{Box as B,Text as p,useStdout as te}from"ink";import{Fragment as re,jsx as l,jsxs as A}from"react/jsx-runtime";var vt=28;function Et(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function ee(){let{stdout:t}=te(),e=t?.columns??80;return Math.max(20,e-vt)}function E({left:t,right:e}){let r=ee();return A(B,{children:[l(B,{width:r,children:t}),l(B,{width:vt,justifyContent:"flex-end",children:e})]})}function Wt(t){let e=[...t.target],r=[...t.typed],n=A(B,{children:[e.map((k,T)=>{let b=T<r.length,d=t.hideTarget&&!b?"_":b?r[T]:k,x=t.error?s.error:b?s.accent:s.muted;return l(p,{bold:!0,color:x,children:d},T)}),t.phonetic&&A(re,{children:[l(p,{children:" "}),l(p,{italic:!0,dimColor:!0,color:s.muted,children:t.phonetic})]})]}),c=t.translation.length>0?l(p,{color:s.primary,children:t.translation[0]}):l(p,{children:" "}),i=t.info,u=Number.isInteger(i.accPct)?`${i.accPct}`:i.accPct.toFixed(1),a=i.visible?l(p,{color:s.muted,children:`${i.dictName} \xB7 ${i.chapterLabel}`}):l(p,{children:" "}),f=i.visible?l(p,{color:s.muted,children:`${i.completed}/${i.total} \xB7 ${i.wpm}wpm \xB7 ${u}%`}):l(p,{children:" "}),y=i.visible?l(p,{color:s.muted,children:Et(i.elapsedMs)}):l(p,{children:" "});return A(B,{flexDirection:"column",children:[l(E,{left:n,right:a}),l(E,{left:l(p,{children:" "}),right:f}),l(E,{left:c,right:y})]})}function At(){let t=M();return A(B,{flexDirection:"column",children:[l(E,{left:l(p,{color:s.warning,children:t.stealth.paused}),right:l(p,{color:s.muted,children:t.stealth.pausedHintRight})}),l(E,{left:l(p,{children:" "}),right:A(p,{color:s.muted,children:["Esc ",t.common.back]})}),l(E,{left:l(p,{children:" "}),right:l(p,{children:" "})})]})}function Pt(t){let e=M(),r=Number.isInteger(t.accPct)?`${t.accPct}`:t.accPct.toFixed(1),n=`${e.stealth.chapterDone} \xB7 ${t.wordCount}w \xB7 ${t.wpm}wpm \xB7 ${r}% \xB7 ${Et(t.durationMs)}`;return A(B,{flexDirection:"column",children:[l(E,{left:l(p,{color:s.success,children:n}),right:l(p,{color:s.muted,children:e.stealth.nextHintRight})}),l(E,{left:l(p,{children:" "}),right:A(p,{color:s.muted,children:["Esc ",e.common.back]})}),l(E,{left:l(p,{children:" "}),right:l(p,{children:" "})})]})}import{jsx as o,jsxs as S}from"react/jsx-runtime";function Je({params:t}){let{dictId:e,chapterIndex:r,mode:n}=t,{cfg:c}=Y(),i=M(),[u,a]=L("loading"),[f,y]=L(null),[k,T]=L(null);return H(()=>{let b=!1;return a("loading"),y(null),T(null),(async()=>{try{let d=await ot(e);if(b)return;if(n==="review"){let j=await V();if(b)return;let O=d.filter(F=>j[F.name]?.count).slice(0,c.chapterSize);if(O.length===0){T(i.practice.errors.noMistakes),a("error");return}y({playlist:O,totalChapters:1}),a("typing");return}let x=yt(d,c.chapterSize);if(x.length===0){T(i.practice.errors.dictEmpty(e)),a("error");return}let N=Math.max(0,Math.min(x.length-1,r)),W=wt(x[N],n);y({playlist:W,totalChapters:x.length}),a("typing")}catch(d){if(b)return;T(d.message),a("error")}})(),()=>{b=!0}},[e,r,n,c.chapterSize,i]),u==="loading"?o(le,{text:i.practice.loading,color:s.muted}):u==="error"?o(ae,{msg:k??i.practice.errors.unknown}):f?o(oe,{params:t,loaded:f,phase:u,setPhase:a},`${e}-${r}-${n}-${t.stealth?"s":"n"}`):null}function oe({params:t,loaded:e,phase:r,setPhase:n}){let{dictId:c,chapterIndex:i,mode:u}=t,a=t.stealth===!0,{cfg:f}=Y(),y=X(),{exit:k}=ne(),T=()=>y.stack.length>1?y.back():k(),b=kt({dictId:c,chapterIndex:i,mode:u}),d=dt(c),x=Mt({enabled:!a&&f.sounds.master,accent:f.accent,autoplayPronunciation:!a&&f.autoplayPronunciation}),N=Z(!1),W=Z(null),j=Z(-1),[O,F]=L(!1),[et,Nt]=L(null);H(()=>{if(et===null)return;let h=setTimeout(()=>F(!1),2e3);return()=>clearTimeout(h)},[et]);let{session:I,lastEffect:v,tick:Rt}=St({playlist:e.playlist,enabled:r==="typing",onComplete:h=>{N.current||(N.current=!0,n("summary"),Promise.resolve(b(Q(h))).catch(w=>{console.error("Failed to persist session:",w)}))},onEscape:()=>n(r==="paused"?"typing":"paused"),onTab:a?void 0:()=>{let h=I.current?e.playlist[I.current.wordIndex]:void 0;h&&x.pronounce(h.name)}});H(()=>{a||v!==null&&v!==W.current&&(W.current=v,v==="wrong"&&f.sounds.feedback&&x.wrong(),v==="progress"&&f.sounds.keystroke&&x.keystroke(),v==="correct"&&(f.sounds.feedback&&x.correct(),f.sounds.keystroke&&x.keystroke()))},[a,v,x,f.sounds.feedback,f.sounds.keystroke]),H(()=>{if(a)return;let h=I.current?.wordIndex??-1;if(h===-1||h===j.current)return;j.current=h;let w=e.playlist[h],$=e.playlist[h+1];w&&f.autoplayPronunciation&&x.pronounce(w.name),$&&x.prefetch($.name)},[a,I.current?.wordIndex,x,f.autoplayPronunciation,e.playlist]),z((h,w)=>{if(w.tab){F(!0),Nt(Date.now());return}},{isActive:a&&r==="typing"}),z((h,w)=>{if(w.return){n("typing");return}if(w.escape){T();return}},{isActive:r==="paused"}),z((h,w)=>{if(w.escape){T();return}if(w.return){let $=i+1;u==="loop"?y.replace({name:"practice",params:{dictId:c,chapterIndex:i,mode:u,stealth:t.stealth}}):u==="review"||$>=e.totalChapters?T():y.replace({name:"practice",params:{dictId:c,chapterIndex:$,mode:u,stealth:t.stealth}});return}if(h==="m"){y.replace({name:"practice",params:{dictId:c,chapterIndex:0,mode:"review",stealth:t.stealth}});return}},{isActive:r==="summary"});let P=I.results.length,Dt=I.results.reduce((h,w)=>h+w.errors,0),U=Date.now()-I.startedAt,rt=U/6e4,nt=rt>0?Math.round(P/rt*10)/10:0,C=r==="summary"?Q(I):null;if(a){if(r==="paused")return o(At,{});if(r==="summary"&&C){let R=C.durationMs/6e4,Vt=R>0?Math.round(C.wordCount/R*10)/10:0,_t=Object.keys(C.perWordErrors).length,Gt=C.wordCount===0?1:Math.max(0,(C.wordCount-_t)/C.wordCount),Ht=Math.round(Gt*1e3)/10;return o(Pt,{wordCount:C.wordCount,errors:C.errors,durationMs:C.durationMs,wpm:Vt,accPct:Ht})}let h=I.current?e.playlist[I.current.wordIndex]:e.playlist[e.playlist.length-1],w=I.current?.input??{target:"",typed:"",errorsThisWord:0},$=new Set(I.results.filter(R=>R.errors>0).map(R=>R.word)).size,jt=P===0?1:Math.max(0,(P-$)/P),Ot=Math.round(jt*1e3)/10,Ft=u==="review"?"review":`ch ${i+1}/${e.totalChapters}`;return o(Wt,{target:h?.name??"",typed:w.typed,hideTarget:u==="dictation",phonetic:$t(h,f.accent),translation:h?.trans??[],error:v==="wrong",info:{visible:O,dictName:D(d,24),chapterLabel:Ft,completed:P,total:e.playlist.length,wpm:nt,accPct:Ot,elapsedMs:U}})}if(r==="paused")return o(se,{dictName:d,chapterIndex:i,totalChapters:e.totalChapters,mode:u,completed:P,total:e.playlist.length});if(r==="summary"&&C)return o(de,{dictName:d,chapterIndex:i,totalChapters:e.totalChapters,mode:u,summary:C});let K=I.current?e.playlist[I.current.wordIndex]:e.playlist[e.playlist.length-1],Lt=I.current?.input??{target:"",typed:"",errorsThisWord:0};return o(ce,{dictName:d,chapterIndex:i,totalChapters:e.totalChapters,mode:u,accent:f.accent,completed:P,total:e.playlist.length,errors:Dt,wpm:nt,elapsedMs:U,target:K?.name??"",typed:Lt.typed,flashError:v==="wrong",hideTarget:u==="dictation",phonetic:$t(K,f.accent),translation:K?.trans??[]})}function $t(t,e){return t?(e==="us"?t.usphone:t.ukphone)??null:null}function tt(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function ce(t){let e=M(),r=t.total===0?0:t.completed/t.total;return S(m,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[o(ie,{dictName:t.dictName,chapterIndex:t.chapterIndex,totalChapters:t.totalChapters,mode:t.mode,accent:t.accent,completed:t.completed,total:t.total,elapsedMs:t.elapsedMs}),S(m,{flexGrow:1,flexDirection:"column",alignItems:"center",justifyContent:"center",children:[o(mt,{target:t.target,typed:t.typed,error:t.flashError,hideTarget:t.hideTarget}),t.phonetic&&o(m,{marginTop:1,children:o(g,{italic:!0,dimColor:!0,color:s.muted,children:t.phonetic})}),t.translation.length>0&&o(m,{marginTop:1,flexDirection:"column",alignItems:"center",children:t.translation.slice(0,2).map((n,c)=>o(g,{color:s.primary,children:n},c))})]}),S(m,{flexDirection:"column",children:[o(Bt,{frac:r}),o(m,{justifyContent:"center",marginTop:1,children:S(g,{color:s.muted,children:[t.completed,"/",t.total," \xB7 ",tt(t.elapsedMs)," \xB7 ",t.wpm," ",e.practice.statCards.wpm," \xB7 ",t.errors," ",e.practice.statCards.errors]})}),o(m,{justifyContent:"center",marginTop:1,children:o(g,{color:s.muted,children:e.practice.footers.typing})})]})]})}function ie(t){let e=M(),r=e.practice.modes[t.mode],n=e.practice.accents[t.accent],c=D(t.dictName,20),i=t.mode==="review"?`${c} \xB7 ${e.practice.reviewLabel} \xB7 ${n}`:`${c} \xB7 ${e.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${r} \xB7 ${n}`,u=`${t.completed}/${t.total} \xB7 ${tt(t.elapsedMs)}`;return S(m,{children:[o(g,{color:s.muted,children:i}),o(m,{flexGrow:1}),o(g,{color:s.muted,children:u})]})}function Bt({frac:t}){let e=process.stdout.columns??80,r=Math.max(20,Math.min(72,e-16)),n=Math.round(r*Math.max(0,Math.min(1,t))),c=r-n;return S(m,{justifyContent:"center",children:[o(g,{color:s.accent,children:"\u2501".repeat(n)}),o(g,{color:s.muted,children:"\u2500".repeat(c)})]})}function se(t){let e=M(),r=t.total===0?0:t.completed/t.total,n=t.mode==="review"?`${D(t.dictName,20)} \xB7 ${e.practice.reviewLabel}`:`${D(t.dictName,20)} \xB7 ${e.practice.pause.chapter(t.chapterIndex+1,t.totalChapters)}`;return S(m,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[o(g,{bold:!0,color:s.warning,children:e.practice.pause.title}),o(m,{marginTop:1,children:o(g,{color:s.muted,children:n})}),o(m,{marginTop:2,children:o(Bt,{frac:r})}),o(m,{marginTop:1,children:o(g,{color:s.muted,children:e.practice.pause.progress(t.completed,t.total)})}),o(m,{marginTop:2,children:o(g,{color:s.muted,children:e.practice.pause.hint})})]})}function ae({msg:t}){let e=M();return S(m,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[o(g,{color:s.error,children:t}),o(m,{marginTop:2,children:S(g,{color:s.muted,children:["Esc ",e.common.back]})}),o(ue,{})]})}function ue(){let t=X();return z((e,r)=>{r.escape&&t.back()}),null}function le({text:t,color:e}){return o(m,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:o(g,{color:e,children:t})})}function de(t){let{summary:e}=t,r=e.durationMs/6e4,n=r>0?Math.round(e.wordCount/r*10)/10:0,c=Object.keys(e.perWordErrors).length,i=e.wordCount===0?1:Math.max(0,(e.wordCount-c)/e.wordCount),u=Math.round(i*1e3)/10,a=M(),f=a.practice.modes[t.mode],y=D(t.dictName,20),k=t.mode==="review"?`${y} \xB7 ${a.practice.reviewLabel}`:`${y} \xB7 ${a.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${f}`,b=`Enter ${t.mode==="loop"?a.practice.summary.loopAgain:t.mode==="review"||t.chapterIndex+1>=t.totalChapters?a.practice.summary.backMenu:a.practice.summary.nextChapter} \xB7 m ${a.practice.summary.reviewMistakes} \xB7 Esc ${a.practice.summary.backMenu}`;return S(m,{flexDirection:"column",alignItems:"center",justifyContent:"center",paddingY:1,width:"100%",height:"100%",children:[o(g,{bold:!0,color:s.success,children:a.practice.chapterComplete}),o(m,{marginTop:1,children:o(g,{color:s.muted,children:k})}),S(m,{marginTop:3,flexDirection:"row",justifyContent:"center",children:[o(G,{label:a.practice.statCards.words,value:String(e.wordCount),color:s.text}),o(G,{label:a.practice.statCards.errors,value:String(e.errors),color:e.errors>0?s.error:s.muted}),o(G,{label:a.practice.statCards.wpm,value:String(n),color:s.accent}),o(G,{label:a.practice.statCards.accuracy,value:`${u}%`,color:s.accent})]}),o(m,{marginTop:2,children:o(g,{color:s.muted,children:a.practice.statCards.elapsed(tt(e.durationMs))})}),o(m,{flexGrow:1}),o(m,{marginTop:2,children:o(g,{color:s.muted,children:b})})]})}function G({label:t,value:e,color:r}){return S(m,{flexDirection:"column",alignItems:"center",marginX:3,children:[o(g,{bold:!0,color:r,children:e}),o(g,{color:s.muted,children:t})]})}export{Je as PracticeScreen};
|
|
2
|
-
//# sourceMappingURL=PracticeScreen-GBCDCFRM.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui/screens/PracticeScreen.tsx","../src/util/shuffle.ts","../src/domain/chapters.ts","../src/domain/input-buffer.ts","../src/domain/session.ts","../src/ui/hooks/useWordLoop.ts","../src/ui/hooks/useAudio.ts","../src/ui/hooks/useSessionPersistence.ts","../src/ui/screens/StealthPracticeLayout.tsx"],"sourcesContent":["import { useState, useEffect, useRef } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport type { Mode } from '../../domain/chapters.js';\nimport { chunkChapters, buildPlaylist } from '../../domain/chapters.js';\nimport { sessionSummary } from '../../domain/session.js';\nimport { loadMistakes } from '../../domain/mistakes.js';\nimport { ensureDictionary } from '../../infra/dict-downloader.js';\nimport { useWordLoop } from '../hooks/useWordLoop.js';\nimport { useAudio } from '../hooks/useAudio.js';\nimport { useSessionPersistence } from '../hooks/useSessionPersistence.js';\nimport { useNav, type PracticeParams } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { BigWord, PALETTE } from '../components/BigWord.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { StealthTyping, StealthPaused, StealthSummary } from './StealthPracticeLayout.js';\n\ntype Phase = 'loading' | 'typing' | 'paused' | 'summary' | 'error';\n\ntype Loaded = {\n playlist: Word[];\n totalChapters: number;\n};\n\nexport function PracticeScreen({ params }: { params: PracticeParams }) {\n const { dictId, chapterIndex, mode } = params;\n const { cfg } = useAppState();\n const t = useStrings();\n\n const [phase, setPhase] = useState<Phase>('loading');\n const [loaded, setLoaded] = useState<Loaded | null>(null);\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n setPhase('loading');\n setLoaded(null);\n setErrorMsg(null);\n (async () => {\n try {\n const words = await ensureDictionary(dictId);\n if (cancelled) return;\n if (mode === 'review') {\n const book = await loadMistakes();\n if (cancelled) return;\n const reviewWords = words.filter((w) => book[w.name]?.count).slice(0, cfg.chapterSize);\n if (reviewWords.length === 0) {\n setErrorMsg(t.practice.errors.noMistakes);\n setPhase('error');\n return;\n }\n setLoaded({ playlist: reviewWords, totalChapters: 1 });\n setPhase('typing');\n return;\n }\n const chapters = chunkChapters(words, cfg.chapterSize);\n if (chapters.length === 0) {\n setErrorMsg(t.practice.errors.dictEmpty(dictId));\n setPhase('error');\n return;\n }\n const idx = Math.max(0, Math.min(chapters.length - 1, chapterIndex));\n const playlist = buildPlaylist(chapters[idx]!, mode);\n setLoaded({ playlist, totalChapters: chapters.length });\n setPhase('typing');\n } catch (err) {\n if (cancelled) return;\n setErrorMsg((err as Error).message);\n setPhase('error');\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [dictId, chapterIndex, mode, cfg.chapterSize, t]);\n\n if (phase === 'loading') {\n return <Centered text={t.practice.loading} color={PALETTE.muted} />;\n }\n if (phase === 'error') {\n return <ErrorView msg={errorMsg ?? t.practice.errors.unknown} />;\n }\n if (!loaded) return null;\n\n return (\n <PracticeRunner\n key={`${dictId}-${chapterIndex}-${mode}-${params.stealth ? 's' : 'n'}`}\n params={params}\n loaded={loaded}\n phase={phase}\n setPhase={setPhase}\n />\n );\n}\n\nfunction PracticeRunner({\n params,\n loaded,\n phase,\n setPhase,\n}: {\n params: PracticeParams;\n loaded: Loaded;\n phase: Phase;\n setPhase: (p: Phase) => void;\n}) {\n const { dictId, chapterIndex, mode } = params;\n const stealth = params.stealth === true;\n const { cfg } = useAppState();\n const nav = useNav();\n const { exit } = useApp();\n const goBack = () => (nav.stack.length > 1 ? nav.back() : exit());\n const persist = useSessionPersistence({ dictId, chapterIndex, mode });\n const dictName = useDictName(dictId);\n\n const audio = useAudio({\n enabled: !stealth && cfg.sounds.master,\n accent: cfg.accent,\n autoplayPronunciation: !stealth && cfg.autoplayPronunciation,\n });\n\n const finishedRef = useRef(false);\n const lastEffectRef = useRef<string | null>(null);\n const lastIndexRef = useRef<number>(-1);\n const [infoVisible, setInfoVisible] = useState(false);\n const [infoShownAt, setInfoShownAt] = useState<number | null>(null);\n\n useEffect(() => {\n if (infoShownAt === null) return;\n const id = setTimeout(() => setInfoVisible(false), 2000);\n return () => clearTimeout(id);\n }, [infoShownAt]);\n\n const { session, lastEffect, tick } = useWordLoop({\n playlist: loaded.playlist,\n enabled: phase === 'typing',\n onComplete: (s) => {\n if (finishedRef.current) return;\n finishedRef.current = true;\n setPhase('summary');\n Promise.resolve(persist(sessionSummary(s))).catch((err) => {\n console.error('Failed to persist session:', err);\n });\n },\n onEscape: () => setPhase(phase === 'paused' ? 'typing' : 'paused'),\n onTab: stealth\n ? undefined\n : () => {\n const cur = session.current ? loaded.playlist[session.current.wordIndex] : undefined;\n if (cur) void audio.pronounce(cur.name);\n },\n });\n\n useEffect(() => {\n if (stealth) return;\n if (lastEffect === null) return;\n if (lastEffect === lastEffectRef.current) return;\n lastEffectRef.current = lastEffect;\n if (lastEffect === 'wrong' && cfg.sounds.feedback) audio.wrong();\n if (lastEffect === 'progress' && cfg.sounds.keystroke) audio.keystroke();\n if (lastEffect === 'correct') {\n if (cfg.sounds.feedback) audio.correct();\n if (cfg.sounds.keystroke) audio.keystroke();\n }\n }, [stealth, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);\n\n useEffect(() => {\n if (stealth) return;\n const idx = session.current?.wordIndex ?? -1;\n if (idx === -1) return;\n if (idx === lastIndexRef.current) return;\n lastIndexRef.current = idx;\n const cur = loaded.playlist[idx];\n const next = loaded.playlist[idx + 1];\n if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);\n if (next) audio.prefetch(next.name);\n }, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);\n\n void tick;\n\n useInput(\n (_input, key) => {\n // Node's readline normalizes byte 0x09 (Ctrl+I) to {name:'tab', ctrl:false},\n // so key.ctrl && input === 'i' would never match. Tab and Ctrl+I both arrive\n // here as key.tab — bind to that. In stealth mode Tab has no other use\n // (onTab is disabled below), so this is non-conflicting.\n if (key.tab) {\n setInfoVisible(true);\n setInfoShownAt(Date.now());\n return;\n }\n },\n { isActive: stealth && phase === 'typing' },\n );\n\n useInput(\n (_input, key) => {\n if (key.return) {\n setPhase('typing');\n return;\n }\n if (key.escape) {\n goBack();\n return;\n }\n },\n { isActive: phase === 'paused' },\n );\n\n useInput(\n (input, key) => {\n if (key.escape) {\n goBack();\n return;\n }\n if (key.return) {\n const nextIdx = chapterIndex + 1;\n if (mode === 'loop') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex, mode, stealth: params.stealth },\n });\n } else if (mode === 'review' || nextIdx >= loaded.totalChapters) {\n goBack();\n } else {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth },\n });\n }\n return;\n }\n if (input === 'm') {\n nav.replace({\n name: 'practice',\n params: { dictId, chapterIndex: 0, mode: 'review', stealth: params.stealth },\n });\n return;\n }\n },\n { isActive: phase === 'summary' },\n );\n\n const completed = session.results.length;\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const elapsedMs = Date.now() - session.startedAt;\n const minutes = elapsedMs / 60000;\n const wpm = minutes > 0 ? Math.round((completed / minutes) * 10) / 10 : 0;\n\n const summary = phase === 'summary' ? sessionSummary(session) : null;\n\n if (stealth) {\n if (phase === 'paused') return <StealthPaused />;\n if (phase === 'summary' && summary) {\n const sMinutes = summary.durationMs / 60000;\n const sWpm = sMinutes > 0 ? Math.round((summary.wordCount / sMinutes) * 10) / 10 : 0;\n const sErrWords = Object.keys(summary.perWordErrors).length;\n const sAcc =\n summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - sErrWords) / summary.wordCount);\n const sAccPct = Math.round(sAcc * 1000) / 10;\n return (\n <StealthSummary\n wordCount={summary.wordCount}\n errors={summary.errors}\n durationMs={summary.durationMs}\n wpm={sWpm}\n accPct={sAccPct}\n />\n );\n }\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n const errWords = new Set(\n session.results.filter((r) => r.errors > 0).map((r) => r.word),\n ).size;\n const accFrac =\n completed === 0 ? 1 : Math.max(0, (completed - errWords) / completed);\n const accPct = Math.round(accFrac * 1000) / 10;\n const chapterLabel =\n mode === 'review'\n ? 'review'\n : `ch ${chapterIndex + 1}/${loaded.totalChapters}`;\n return (\n <StealthTyping\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n error={lastEffect === 'wrong'}\n info={{\n visible: infoVisible,\n dictName: truncateName(dictName, 24),\n chapterLabel,\n completed,\n total: loaded.playlist.length,\n wpm,\n accPct,\n elapsedMs,\n }}\n />\n );\n }\n\n if (phase === 'paused') {\n return (\n <PausedView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n completed={completed}\n total={loaded.playlist.length}\n />\n );\n }\n\n if (phase === 'summary' && summary) {\n return (\n <SummaryView\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n summary={summary}\n />\n );\n }\n\n const currentWord = session.current\n ? loaded.playlist[session.current.wordIndex]\n : loaded.playlist[loaded.playlist.length - 1];\n const inputState = session.current?.input ?? { target: '', typed: '', errorsThisWord: 0 };\n\n return (\n <TypingLayout\n dictName={dictName}\n chapterIndex={chapterIndex}\n totalChapters={loaded.totalChapters}\n mode={mode}\n accent={cfg.accent}\n completed={completed}\n total={loaded.playlist.length}\n errors={errors}\n wpm={wpm}\n elapsedMs={elapsedMs}\n target={currentWord?.name ?? ''}\n typed={inputState.typed}\n flashError={lastEffect === 'wrong'}\n hideTarget={mode === 'dictation'}\n phonetic={pickPhonetic(currentWord, cfg.accent)}\n translation={currentWord?.trans ?? []}\n />\n );\n}\n\nfunction pickPhonetic(word: Word | undefined, accent: 'us' | 'uk'): string | null {\n if (!word) return null;\n const p = accent === 'us' ? word.usphone : word.ukphone;\n return p ?? null;\n}\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction TypingLayout(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n errors: number;\n wpm: number;\n elapsedMs: number;\n target: string;\n typed: string;\n flashError: boolean;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n}) {\n const t = useStrings();\n const progressFrac = props.total === 0 ? 0 : props.completed / props.total;\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <StatusBar\n dictName={props.dictName}\n chapterIndex={props.chapterIndex}\n totalChapters={props.totalChapters}\n mode={props.mode}\n accent={props.accent}\n completed={props.completed}\n total={props.total}\n elapsedMs={props.elapsedMs}\n />\n\n <Box flexGrow={1} flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\">\n <BigWord\n target={props.target}\n typed={props.typed}\n error={props.flashError}\n hideTarget={props.hideTarget}\n />\n\n {props.phonetic && (\n <Box marginTop={1}>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </Box>\n )}\n\n {props.translation.length > 0 && (\n <Box marginTop={1} flexDirection=\"column\" alignItems=\"center\">\n {props.translation.slice(0, 2).map((tr, i) => (\n <Text key={i} color={PALETTE.primary}>\n {tr}\n </Text>\n ))}\n </Box>\n )}\n </Box>\n\n <Box flexDirection=\"column\">\n <ProgressBar frac={progressFrac} />\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>\n {props.completed}/{props.total} · {fmtTime(props.elapsedMs)} · {props.wpm} {t.practice.statCards.wpm} · {props.errors} {t.practice.statCards.errors}\n </Text>\n </Box>\n <Box justifyContent=\"center\" marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.footers.typing}</Text>\n </Box>\n </Box>\n </Box>\n );\n}\n\nfunction StatusBar(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n accent: 'us' | 'uk';\n completed: number;\n total: number;\n elapsedMs: number;\n}) {\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const accentName = t.practice.accents[props.accent];\n const name = truncateName(props.dictName, 20);\n const left =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel} · ${accentName}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName} · ${accentName}`;\n const right = `${props.completed}/${props.total} · ${fmtTime(props.elapsedMs)}`;\n return (\n <Box>\n <Text color={PALETTE.muted}>{left}</Text>\n <Box flexGrow={1} />\n <Text color={PALETTE.muted}>{right}</Text>\n </Box>\n );\n}\n\nfunction ProgressBar({ frac }: { frac: number }) {\n const cols = process.stdout.columns ?? 80;\n const width = Math.max(20, Math.min(72, cols - 16));\n const filled = Math.round(width * Math.max(0, Math.min(1, frac)));\n const empty = width - filled;\n return (\n <Box justifyContent=\"center\">\n <Text color={PALETTE.accent}>{'━'.repeat(filled)}</Text>\n <Text color={PALETTE.muted}>{'─'.repeat(empty)}</Text>\n </Box>\n );\n}\n\nfunction PausedView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n completed: number;\n total: number;\n}) {\n const t = useStrings();\n const frac = props.total === 0 ? 0 : props.completed / props.total;\n const subtitle =\n props.mode === 'review'\n ? `${truncateName(props.dictName, 20)} · ${t.practice.reviewLabel}`\n : `${truncateName(props.dictName, 20)} · ${t.practice.pause.chapter(props.chapterIndex + 1, props.totalChapters)}`;\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.warning}>\n {t.practice.pause.title}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n <Box marginTop={2}>\n <ProgressBar frac={frac} />\n </Box>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.practice.pause.progress(props.completed, props.total)}</Text>\n </Box>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.pause.hint}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction ErrorView({ msg }: { msg: string }) {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.error}>{msg}</Text>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>Esc {t.common.back}</Text>\n </Box>\n <BackKey />\n </Box>\n );\n}\n\nfunction BackKey() {\n const nav = useNav();\n useInput((_input, key) => {\n if (key.escape) nav.back();\n });\n return null;\n}\n\nfunction Centered({ text, color }: { text: string; color: string }) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={color}>{text}</Text>\n </Box>\n );\n}\n\nfunction SummaryView(props: {\n dictName: string;\n chapterIndex: number;\n totalChapters: number;\n mode: Mode;\n summary: { wordCount: number; errors: number; durationMs: number; perWordErrors: Record<string, number> };\n}) {\n const { summary } = props;\n const minutes = summary.durationMs / 60000;\n const wpm = minutes > 0 ? Math.round((summary.wordCount / minutes) * 10) / 10 : 0;\n const errorWords = Object.keys(summary.perWordErrors).length;\n const acc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - errorWords) / summary.wordCount);\n const accPct = Math.round(acc * 1000) / 10;\n\n const t = useStrings();\n const modeName = t.practice.modes[props.mode];\n const name = truncateName(props.dictName, 20);\n const subtitle =\n props.mode === 'review'\n ? `${name} · ${t.practice.reviewLabel}`\n : `${name} · ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} · ${modeName}`;\n\n const nextLabel =\n props.mode === 'loop'\n ? t.practice.summary.loopAgain\n : props.mode === 'review' || props.chapterIndex + 1 >= props.totalChapters\n ? t.practice.summary.backMenu\n : t.practice.summary.nextChapter;\n const footer = `Enter ${nextLabel} · m ${t.practice.summary.reviewMistakes} · Esc ${t.practice.summary.backMenu}`;\n\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.success}>\n {t.practice.chapterComplete}\n </Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{subtitle}</Text>\n </Box>\n\n <Box marginTop={3} flexDirection=\"row\" justifyContent=\"center\">\n <StatCard label={t.practice.statCards.words} value={String(summary.wordCount)} color={PALETTE.text} />\n <StatCard\n label={t.practice.statCards.errors}\n value={String(summary.errors)}\n color={summary.errors > 0 ? PALETTE.error : PALETTE.muted}\n />\n <StatCard label={t.practice.statCards.wpm} value={String(wpm)} color={PALETTE.accent} />\n <StatCard label={t.practice.statCards.accuracy} value={`${accPct}%`} color={PALETTE.accent} />\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{t.practice.statCards.elapsed(fmtTime(summary.durationMs))}</Text>\n </Box>\n\n <Box flexGrow={1} />\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>{footer}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction StatCard({ label, value, color }: { label: string; value: string; color: string }) {\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" marginX={3}>\n <Text bold color={color}>{value}</Text>\n <Text color={PALETTE.muted}>{label}</Text>\n </Box>\n );\n}\n","export function shuffle<T>(arr: readonly T[], rng: () => number = Math.random): T[] {\n const out = [...arr];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(rng() * (i + 1));\n const tmp = out[i]!;\n out[i] = out[j]!;\n out[j] = tmp;\n }\n return out;\n}\n\nexport function mulberry32(seed: number): () => number {\n let t = seed >>> 0;\n return () => {\n t = (t + 0x6d2b79f5) >>> 0;\n let r = Math.imul(t ^ (t >>> 15), 1 | t);\n r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;\n return ((r ^ (r >>> 14)) >>> 0) / 4294967296;\n };\n}\n","import type { Word } from './dictionary.js';\nimport { shuffle, mulberry32 } from '../util/shuffle.js';\n\nexport type Mode = 'order' | 'dictation' | 'review' | 'random' | 'loop';\n\nexport function chunkChapters(words: Word[], chapterSize: number): Word[][] {\n if (chapterSize <= 0) throw new Error('chapterSize must be positive');\n const chunks: Word[][] = [];\n for (let i = 0; i < words.length; i += chapterSize) {\n chunks.push(words.slice(i, i + chapterSize));\n }\n return chunks;\n}\n\nexport function chapterCount(totalWords: number, chapterSize: number): number {\n return Math.ceil(totalWords / chapterSize);\n}\n\n/**\n * Build a play list for a chapter, applying the given mode.\n * - order: return as-is\n * - dictation: same as order (hide-the-word behavior is UI-only)\n * - random: single shuffled pass\n * - loop: return as-is; the practice screen drives the repeat\n * - review: caller passes the mistake-book words, we just chunk those\n */\nexport function buildPlaylist(chapter: Word[], mode: Mode, seed?: number): Word[] {\n if (mode === 'random') {\n const rng = seed === undefined ? Math.random : mulberry32(seed);\n return shuffle(chapter, rng);\n }\n return chapter;\n}\n","export type InputState = {\n target: string;\n typed: string;\n errorsThisWord: number;\n};\n\nexport type InputEvent =\n | { type: 'char'; ch: string }\n | { type: 'backspace' }\n | { type: 'reset' };\n\nexport type InputEffect = 'none' | 'progress' | 'wrong' | 'correct' | 'skipped';\n\nexport function initialState(target: string): InputState {\n return { target, typed: '', errorsThisWord: 0 };\n}\n\nexport function reduce(state: InputState, ev: InputEvent): { state: InputState; effect: InputEffect } {\n switch (ev.type) {\n case 'reset':\n return { state: { ...state, typed: '' }, effect: 'none' };\n case 'backspace': {\n if (state.typed.length === 0) return { state, effect: 'none' };\n return { state: { ...state, typed: state.typed.slice(0, -1) }, effect: 'none' };\n }\n case 'char': {\n const candidate = state.typed + ev.ch;\n // Compare by code-point index, not byte length, to handle unicode safely.\n const targetUpToCandidate = [...state.target].slice(0, [...candidate].length).join('');\n if (candidate === targetUpToCandidate) {\n if (candidate.length === state.target.length) {\n return { state: { ...state, typed: candidate }, effect: 'correct' };\n }\n return { state: { ...state, typed: candidate }, effect: 'progress' };\n }\n return {\n state: { ...state, typed: '', errorsThisWord: state.errorsThisWord + 1 },\n effect: 'wrong',\n };\n }\n }\n}\n","import type { Word } from './dictionary.js';\nimport { initialState, reduce, type InputEvent, type InputState, type InputEffect } from './input-buffer.js';\n\nexport type SessionWordResult = { word: string; errors: number; durationMs: number; skipped?: boolean };\n\nexport type Session = {\n startedAt: number;\n results: SessionWordResult[];\n current: { wordIndex: number; wordStartedAt: number; input: InputState } | null;\n finishedAt: number | null;\n playlist: Word[];\n};\n\nexport function startSession(playlist: Word[], now = Date.now()): Session {\n if (playlist.length === 0) {\n return { startedAt: now, results: [], current: null, finishedAt: now, playlist };\n }\n return {\n startedAt: now,\n results: [],\n current: { wordIndex: 0, wordStartedAt: now, input: initialState(playlist[0]!.name) },\n finishedAt: null,\n playlist,\n };\n}\n\nexport function feedSession(session: Session, ev: InputEvent, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const { state, effect } = reduce(session.current.input, ev);\n if (effect === 'correct') {\n const finished: SessionWordResult = {\n word: state.target,\n errors: state.errorsThisWord,\n durationMs: now - session.current.wordStartedAt,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, finished];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect,\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect,\n };\n }\n return {\n session: {\n ...session,\n current: { ...session.current, input: state },\n },\n effect,\n };\n}\n\nexport function skipSession(session: Session, now = Date.now()): { session: Session; effect: InputEffect } {\n if (!session.current) return { session, effect: 'none' };\n const result: SessionWordResult = {\n word: session.current.input.target,\n errors: 0,\n durationMs: now - session.current.wordStartedAt,\n skipped: true,\n };\n const nextIndex = session.current.wordIndex + 1;\n const results = [...session.results, result];\n if (nextIndex >= session.playlist.length) {\n return {\n session: { ...session, results, current: null, finishedAt: now },\n effect: 'skipped',\n };\n }\n return {\n session: {\n ...session,\n results,\n current: {\n wordIndex: nextIndex,\n wordStartedAt: now,\n input: initialState(session.playlist[nextIndex]!.name),\n },\n },\n effect: 'skipped',\n };\n}\n\nexport function sessionSummary(session: Session): {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n} {\n const errors = session.results.reduce((a, r) => a + r.errors, 0);\n const durationMs =\n (session.finishedAt ?? Date.now()) - session.startedAt;\n const perWordErrors: Record<string, number> = {};\n for (const r of session.results) {\n if (r.errors > 0) perWordErrors[r.word] = (perWordErrors[r.word] ?? 0) + r.errors;\n }\n return { wordCount: session.results.length, errors, durationMs, perWordErrors };\n}\n","import { useEffect, useReducer, useRef, useState } from 'react';\nimport { useInput, useApp } from 'ink';\nimport type { Word } from '../../domain/dictionary.js';\nimport { startSession, feedSession, skipSession, type Session } from '../../domain/session.js';\nimport type { InputEffect } from '../../domain/input-buffer.js';\n\ntype Action =\n | { type: 'event'; input: string; key: { backspace?: boolean; delete?: boolean; tab?: boolean; escape?: boolean; return?: boolean; ctrl?: boolean }; now: number }\n | { type: 'start'; playlist: Word[]; now: number }\n | { type: 'skip'; now: number };\n\nfunction reducer(state: { session: Session; lastEffect: InputEffect | null }, action: Action) {\n if (action.type === 'start') {\n return { session: startSession(action.playlist, action.now), lastEffect: null };\n }\n if (action.type === 'skip') {\n const r = skipSession(state.session, action.now);\n return { session: r.session, lastEffect: r.effect };\n }\n if (action.type === 'event') {\n if (action.key.backspace || action.key.delete) {\n const r = feedSession(state.session, { type: 'backspace' }, action.now);\n return { session: r.session, lastEffect: r.effect };\n }\n if (action.input.length === 0) return state;\n let session = state.session;\n let lastEffect: InputEffect | null = state.lastEffect;\n for (const c of action.input) {\n const r = feedSession(session, { type: 'char', ch: c }, action.now);\n session = r.session;\n lastEffect = r.effect;\n if (session.finishedAt !== null) break;\n }\n return { session, lastEffect };\n }\n return state;\n}\n\nexport type UseWordLoopOpts = {\n playlist: Word[];\n onComplete: (session: Session) => void;\n onTab?: () => void;\n onEscape?: () => void;\n onSkip?: () => void;\n enabled?: boolean;\n};\n\nexport function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled = true }: UseWordLoopOpts) {\n const [state, dispatch] = useReducer(reducer, undefined, () => ({\n session: startSession(playlist, Date.now()),\n lastEffect: null as InputEffect | null,\n }));\n const completedRef = useRef(false);\n const [tick, setTick] = useState(0);\n const { exit } = useApp();\n\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n exit();\n return;\n }\n if (key.ctrl && input === 'n') {\n onSkip?.();\n dispatch({ type: 'skip', now: Date.now() });\n return;\n }\n if (key.escape) {\n onEscape?.();\n return;\n }\n if (key.tab) {\n onTab?.();\n return;\n }\n if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return) return;\n if (key.ctrl || key.meta) return;\n const cleaned = [...input].filter((c) => {\n const code = c.codePointAt(0)!;\n return code === 0x20 || (code >= 0x21 && code !== 0x7f);\n }).join('');\n if (cleaned.length === 0) return;\n dispatch({ type: 'event', input: cleaned, key, now: Date.now() });\n },\n { isActive: enabled },\n );\n\n useEffect(() => {\n if (state.session.finishedAt !== null && !completedRef.current) {\n completedRef.current = true;\n onComplete(state.session);\n }\n }, [state.session, onComplete]);\n\n useEffect(() => {\n if (state.session.finishedAt !== null) return;\n const id = setInterval(() => setTick((t) => t + 1), 1000);\n return () => clearInterval(id);\n }, [state.session.finishedAt]);\n\n return { session: state.session, lastEffect: state.lastEffect, tick };\n}\n","import { useEffect, useRef } from 'react';\nimport {\n initAudio,\n playCorrect,\n playWrong,\n playKeystroke,\n playPronunciation,\n prefetchPronunciation,\n} from '../../infra/audio.js';\n\ntype Opts = {\n enabled: boolean;\n accent: 'us' | 'uk';\n autoplayPronunciation: boolean;\n};\n\nexport type AudioControls = {\n keystroke: () => void;\n correct: () => void;\n wrong: () => void;\n pronounce: (word: string) => void;\n prefetch: (word: string) => void;\n};\n\nexport function useAudio(opts: Opts): AudioControls {\n const initedRef = useRef(false);\n useEffect(() => {\n if (initedRef.current) return;\n initedRef.current = true;\n initAudio(!opts.enabled).catch(() => undefined);\n }, [opts.enabled]);\n\n return {\n keystroke: () => opts.enabled && playKeystroke(),\n correct: () => opts.enabled && playCorrect(),\n wrong: () => opts.enabled && playWrong(),\n pronounce: (word) => {\n if (!opts.enabled) return;\n if (opts.autoplayPronunciation) void playPronunciation(word, opts.accent);\n },\n prefetch: (word) => {\n if (!opts.enabled) return;\n void prefetchPronunciation(word, opts.accent);\n },\n };\n}\n","import { useCallback } from 'react';\nimport { appendSession, type SessionRecord } from '../../domain/stats.js';\nimport { loadMistakes, saveMistakes, bump } from '../../domain/mistakes.js';\nimport { addChapter as trackChapter } from '../../infra/session-tracker.js';\nimport type { Mode } from '../../domain/chapters.js';\n\ntype Summary = {\n wordCount: number;\n errors: number;\n durationMs: number;\n perWordErrors: Record<string, number>;\n};\n\nexport function useSessionPersistence(meta: { dictId: string; chapterIndex: number; mode: Mode }) {\n return useCallback(\n async (summary: Summary): Promise<void> => {\n const rec: SessionRecord = {\n ts: new Date().toISOString(),\n dictId: meta.dictId,\n chapter: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n };\n await appendSession(rec);\n trackChapter({\n dictId: meta.dictId,\n chapterIndex: meta.chapterIndex,\n mode: meta.mode,\n wordCount: summary.wordCount,\n errors: summary.errors,\n durationMs: summary.durationMs,\n perWordErrors: summary.perWordErrors,\n });\n const dirty = Object.entries(summary.perWordErrors).filter(([, n]) => n > 0);\n if (dirty.length === 0) return;\n let book = await loadMistakes();\n for (const [word, n] of dirty) book = bump(book, word, meta.dictId, n);\n await saveMistakes(book);\n },\n [meta.dictId, meta.chapterIndex, meta.mode],\n );\n}\n","import type { ReactNode } from 'react';\nimport { Box, Text, useStdout } from 'ink';\nimport { PALETTE } from '../components/BigWord.js';\nimport { useStrings } from '../../i18n/context.js';\n\nconst RIGHT_WIDTH = 28;\n\nfunction fmtTime(ms: number): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n return `${m}:${String(s).padStart(2, '0')}`;\n}\n\nfunction useLeftWidth(): number {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n return Math.max(20, cols - RIGHT_WIDTH);\n}\n\nfunction Row({ left, right }: { left: ReactNode; right: ReactNode }) {\n const leftWidth = useLeftWidth();\n return (\n <Box>\n <Box width={leftWidth}>{left}</Box>\n <Box width={RIGHT_WIDTH} justifyContent=\"flex-end\">\n {right}\n </Box>\n </Box>\n );\n}\n\nexport function StealthTyping(props: {\n target: string;\n typed: string;\n hideTarget: boolean;\n phonetic: string | null;\n translation: string[];\n error: boolean;\n info: {\n visible: boolean;\n dictName: string;\n chapterLabel: string;\n completed: number;\n total: number;\n wpm: number;\n accPct: number;\n elapsedMs: number;\n };\n}) {\n const target = [...props.target];\n const typed = [...props.typed];\n\n // Row 1: word chars + inline phonetic (two-space gap, italic + dim muted).\n // error=true flashes the entire word red; phonetic remains dim regardless.\n const wordCell = (\n <Box>\n {target.map((ch, i) => {\n const isTyped = i < typed.length;\n const display = props.hideTarget && !isTyped ? '_' : isTyped ? typed[i]! : ch;\n const color = props.error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n })}\n {props.phonetic && (\n <>\n <Text> </Text>\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </Text>\n </>\n )}\n </Box>\n );\n\n // Row 3: first translation in primary cyan (matches TypingLayout translation color)\n const translationCell = props.translation.length > 0 ? (\n <Text color={PALETTE.primary}>{props.translation[0]!}</Text>\n ) : (\n <Text> </Text>\n );\n\n const info = props.info;\n const accFmt = Number.isInteger(info.accPct) ? `${info.accPct}` : info.accPct.toFixed(1);\n\n // Right column: idle = all blank; info-visible = dict / progress / time.\n const right1 = info.visible ? (\n <Text color={PALETTE.muted}>{`${info.dictName} · ${info.chapterLabel}`}</Text>\n ) : (\n <Text> </Text>\n );\n const right2 = info.visible ? (\n <Text color={PALETTE.muted}>{`${info.completed}/${info.total} · ${info.wpm}wpm · ${accFmt}%`}</Text>\n ) : (\n <Text> </Text>\n );\n const right3 = info.visible ? (\n <Text color={PALETTE.muted}>{fmtTime(info.elapsedMs)}</Text>\n ) : (\n <Text> </Text>\n );\n\n // Layout: 3 rendered rows. Middle row's left is blank → acts as a 1-line\n // visual gap between word+phonetic and translation. Right column keeps all\n // three info slots (dict / progress / time) when info.visible is true.\n return (\n <Box flexDirection=\"column\">\n <Row left={wordCell} right={right1} />\n <Row left={<Text> </Text>} right={right2} />\n <Row left={translationCell} right={right3} />\n </Box>\n );\n}\n\nexport function StealthPaused() {\n const t = useStrings();\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.warning}>{t.stealth.paused}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.pausedHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n\nexport function StealthSummary(props: {\n wordCount: number;\n errors: number;\n durationMs: number;\n wpm: number;\n accPct: number;\n}) {\n const t = useStrings();\n const accFmt = Number.isInteger(props.accPct) ? `${props.accPct}` : props.accPct.toFixed(1);\n const line = `${t.stealth.chapterDone} · ${props.wordCount}w · ${props.wpm}wpm · ${accFmt}% · ${fmtTime(props.durationMs)}`;\n return (\n <Box flexDirection=\"column\">\n <Row\n left={<Text color={PALETTE.success}>{line}</Text>}\n right={<Text color={PALETTE.muted}>{t.stealth.nextHintRight}</Text>}\n />\n <Row left={<Text> </Text>} right={<Text color={PALETTE.muted}>Esc {t.common.back}</Text>} />\n <Row left={<Text> </Text>} right={<Text> </Text>} />\n </Box>\n );\n}\n"],"mappings":"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","names":["useState","useEffect","useRef","Box","Text","useApp","useInput","shuffle","arr","rng","out","i","j","tmp","mulberry32","seed","t","chunkChapters","words","chapterSize","chunks","i","buildPlaylist","chapter","mode","seed","rng","mulberry32","shuffle","initialState","target","reduce","state","ev","candidate","targetUpToCandidate","startSession","playlist","now","initialState","feedSession","session","ev","state","effect","reduce","finished","nextIndex","results","skipSession","result","sessionSummary","errors","a","r","durationMs","perWordErrors","useEffect","useReducer","useRef","useState","useInput","useApp","reducer","state","action","startSession","skipSession","r","feedSession","session","lastEffect","useWordLoop","playlist","onComplete","onTab","onEscape","onSkip","enabled","dispatch","useReducer","completedRef","useRef","tick","setTick","useState","exit","useApp","useInput","input","key","cleaned","c","code","useEffect","id","t","useEffect","useRef","useAudio","opts","initedRef","useRef","useEffect","initAudio","playKeystroke","playCorrect","playWrong","word","playPronunciation","prefetchPronunciation","useCallback","useSessionPersistence","meta","useCallback","summary","rec","appendSession","addChapter","dirty","n","book","loadMistakes","word","bump","saveMistakes","Box","Text","useStdout","Fragment","jsx","jsxs","RIGHT_WIDTH","fmtTime","ms","total","m","s","useLeftWidth","stdout","useStdout","cols","Row","left","right","leftWidth","Box","StealthTyping","props","target","typed","wordCell","ch","i","isTyped","display","color","PALETTE","Text","translationCell","info","accFmt","right1","right2","right3","StealthPaused","useStrings","StealthSummary","t","line","jsx","jsxs","PracticeScreen","params","dictId","chapterIndex","mode","cfg","useAppState","t","useStrings","phase","setPhase","useState","loaded","setLoaded","errorMsg","setErrorMsg","useEffect","cancelled","words","ensureDictionary","book","loadMistakes","reviewWords","w","chapters","chunkChapters","idx","playlist","buildPlaylist","err","Centered","PALETTE","ErrorView","PracticeRunner","stealth","nav","useNav","exit","useApp","goBack","persist","useSessionPersistence","dictName","useDictName","audio","useAudio","finishedRef","useRef","lastEffectRef","lastIndexRef","infoVisible","setInfoVisible","infoShownAt","setInfoShownAt","id","session","lastEffect","tick","useWordLoop","s","sessionSummary","cur","next","useInput","_input","key","input","nextIdx","completed","errors","a","r","elapsedMs","minutes","wpm","summary","StealthPaused","sMinutes","sWpm","sErrWords","sAcc","sAccPct","StealthSummary","currentWord","inputState","errWords","accFrac","accPct","chapterLabel","StealthTyping","pickPhonetic","truncateName","PausedView","SummaryView","TypingLayout","word","accent","fmtTime","ms","total","m","props","progressFrac","Box","StatusBar","BigWord","Text","tr","i","ProgressBar","modeName","accentName","name","left","right","frac","cols","width","filled","empty","subtitle","msg","BackKey","text","color","errorWords","acc","footer","StatCard","label","value"]}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{b as C,c as N,d as $,e as O,g as j}from"./chunk-MPE25TTQ.js";import{a as P,d as R}from"./chunk-UPA4JFCH.js";import{b as v,d as S}from"./chunk-2MRNI465.js";import{b as A,d as L,f as o}from"./chunk-QEX27D7F.js";import"./chunk-6QICLHIY.js";import"./chunk-6KRVNT2S.js";import{useEffect as _,useState as B}from"react";import{Box as a,Text as r,useInput as G,useStdout as U}from"ink";import{jsx as t,jsxs as s}from"react/jsx-runtime";var T=[7,14,30,90];function ct(){let i=A(),e=L(),[n,u]=B(null),[p,f]=B(null),[d,h]=B(1);if(_(()=>{(async()=>{let[l,c]=await Promise.all([C(),P()]);u(l),f(c)})()},[]),G((l,c)=>{if(c.escape){i.back();return}c.rightArrow&&h(w=>(w+1)%T.length),c.leftArrow&&h(w=>(w-1+T.length)%T.length)}),!n||!p)return t(a,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:t(r,{color:o.muted,children:e.stats.loading})});if(n.length===0)return s(a,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[t(r,{color:o.muted,children:e.stats.none}),t(a,{marginTop:1,children:t(r,{color:o.muted,children:e.stats.nonePractice})}),t(a,{marginTop:2,children:s(r,{color:o.muted,children:["Esc ",e.common.back]})})]});let m=T[d],W=O(n),b=n.reduce((l,c)=>l+c.wordCount,0),V=n.reduce((l,c)=>l+c.errors,0),k=n.reduce((l,c)=>l+c.durationMs,0),Y=n.reduce((l,c)=>l+(c.wordCount-Object.keys(c.perWordErrors).length),0),F=k>0?Math.round(b/(k/6e4)*10)/10:0,H=b===0?1:Y/b,X=n.slice(-5).reverse(),y=R(p,8),x=j(n,m),D=x.peakWpmLifetime>0?Math.min(1,x.avgWpm/x.peakWpmLifetime):0,E=Math.min(1,x.avgAccPct/100),I=m>0?Math.min(1,x.sessionsInWindow/m):0;return s(a,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[t(r,{bold:!0,color:o.accent,children:e.stats.title}),s(a,{marginTop:1,flexDirection:"column",children:[t(r,{color:o.muted,children:e.stats.lifetime}),s(a,{marginTop:1,children:[t(g,{label:e.stats.sessions,value:String(n.length)}),t(g,{label:e.stats.words,value:String(b)}),t(g,{label:e.stats.errors,value:String(V)}),t(g,{label:e.stats.wpm,value:String(F),accent:!0}),t(g,{label:e.stats.accuracy,value:`${Math.round(H*1e3)/10}%`,accent:!0}),t(g,{label:e.stats.streak,value:`${W}d`,accent:!0})]})]}),s(a,{marginTop:2,flexDirection:"column",children:[t(r,{color:o.muted,children:e.stats.last(m)}),s(a,{marginTop:1,flexDirection:"column",borderStyle:"round",borderColor:o.muted,paddingX:2,paddingY:0,children:[t(M,{label:e.stats.bars.speed,frac:D,valueText:`${x.avgWpm} wpm`,pct:Math.round(D*100)}),t(M,{label:e.stats.bars.accuracy,frac:E,valueText:`${x.avgAccPct}%`,pct:Math.round(E*100)}),t(M,{label:e.stats.bars.sessions,frac:I,valueText:`${x.sessionsInWindow} / ${m}`,pct:Math.round(I*100)})]})]}),s(a,{marginTop:2,flexDirection:"column",children:[t(r,{color:o.muted,children:e.stats.recent}),X.map((l,c)=>t(q,{session:l,units:e.stats.recentUnits},c))]}),y.length>0&&s(a,{marginTop:2,flexDirection:"column",children:[t(r,{color:o.muted,children:e.stats.topMistakes}),y.map(([l,c])=>t(z,{word:l,count:c.count,dictIds:c.dictIds,multiSuffix:e.stats.multiDictSuffix},l))]}),t(a,{flexGrow:1}),t(a,{marginTop:1,children:t(r,{color:o.muted,children:e.stats.footer})})]})}function q({session:i,units:e}){let n=v(i.dictId),u=S(n,14);return s(a,{children:[s(r,{color:o.muted,children:[" ",i.ts.replace("T"," ").slice(0,16)," "]}),t(r,{color:o.text,children:u.padEnd(14)}),s(r,{color:o.muted,children:[" ","ch",String(i.chapter+1).padStart(3)," ",i.mode.padEnd(9)," ",String(i.wordCount).padStart(3),e.words," ",i.errors,e.errors," ",N(i),e.wpm," ",Math.round($(i)*1e3)/10,"%"]})]})}function z({word:i,count:e,dictIds:n,multiSuffix:u}){let p=n[0]??"",f=v(p),d=n.length>1?u(n.length-1):"";return s(a,{children:[s(r,{color:o.error,children:[" ",String(e).padStart(3)," "]}),t(r,{color:o.text,children:i.padEnd(20)}),s(r,{color:o.muted,children:[S(f,20),d]})]})}function g({label:i,value:e,accent:n=!1}){return s(a,{marginRight:3,children:[s(r,{color:o.muted,children:[i," "]}),t(r,{bold:!0,color:n?o.accent:o.text,children:e})]})}function J(i){return{barWidth:Math.max(0,Math.min(60,i-10-24)),labelWidth:9,valueWidth:8,pctWidth:5,marginLeft:2}}function M({label:i,frac:e,valueText:n,pct:u}){let{stdout:p}=U(),f=p?.columns??80,d=J(f),h=Math.max(0,Math.min(1,e)),m=Math.round(d.barWidth*h),W=d.barWidth-m;return s(a,{children:[t(a,{width:d.labelWidth,children:t(r,{color:o.muted,children:i})}),t(r,{color:o.accent,children:"\u2501".repeat(m)}),t(r,{color:o.muted,children:"\u2500".repeat(W)}),t(a,{width:d.valueWidth,marginLeft:d.marginLeft,children:t(r,{color:o.text,children:n})}),t(a,{width:d.pctWidth,justifyContent:"flex-end",children:s(r,{color:o.muted,children:[u,"%"]})})]})}export{ct as StatsViewer,J as calcMetricBarLayout};
|
|
2
|
-
//# sourceMappingURL=StatsViewer-IEHZS6GX.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui/screens/StatsViewer.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { Box, Text, useInput, useStdout } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport {\n loadSessions,\n computeWPM,\n accuracy,\n windowAggregate,\n dailyStreak,\n type SessionRecord,\n} from '../../domain/stats.js';\nimport { loadMistakes, topN, type MistakeBook } from '../../domain/mistakes.js';\nimport { truncateName } from '../../util/dict-name.js';\n\nconst DAY_WINDOWS = [7, 14, 30, 90];\n\nexport function StatsViewer() {\n const nav = useNav();\n const t = useStrings();\n const [sessions, setSessions] = useState<SessionRecord[] | null>(null);\n const [book, setBook] = useState<MistakeBook | null>(null);\n const [windowIdx, setWindowIdx] = useState(1);\n\n useEffect(() => {\n void (async () => {\n const [s, b] = await Promise.all([loadSessions(), loadMistakes()]);\n setSessions(s);\n setBook(b);\n })();\n }, []);\n\n useInput((_input, key) => {\n if (key.escape) {\n nav.back();\n return;\n }\n if (key.rightArrow) setWindowIdx((i) => (i + 1) % DAY_WINDOWS.length);\n if (key.leftArrow) setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);\n });\n\n if (!sessions || !book) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>{t.stats.loading}</Text>\n </Box>\n );\n }\n\n if (sessions.length === 0) {\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>{t.stats.none}</Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.stats.nonePractice}</Text>\n </Box>\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>Esc {t.common.back}</Text>\n </Box>\n </Box>\n );\n }\n\n const days = DAY_WINDOWS[windowIdx]!;\n const streak = dailyStreak(sessions);\n const totalWords = sessions.reduce((a, s) => a + s.wordCount, 0);\n const totalErrors = sessions.reduce((a, s) => a + s.errors, 0);\n const totalMs = sessions.reduce((a, s) => a + s.durationMs, 0);\n const firstTryWords = sessions.reduce(\n (a, s) => a + (s.wordCount - Object.keys(s.perWordErrors).length),\n 0,\n );\n const overallWpm = totalMs > 0 ? Math.round((totalWords / (totalMs / 60000)) * 10) / 10 : 0;\n const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;\n\n const recent = sessions.slice(-5).reverse();\n const top = topN(book, 8);\n\n const agg = windowAggregate(sessions, days);\n const speedPct = agg.peakWpmLifetime > 0 ? Math.min(1, agg.avgWpm / agg.peakWpmLifetime) : 0;\n const accPctFrac = Math.min(1, agg.avgAccPct / 100);\n const sessionsPctFrac = days > 0 ? Math.min(1, agg.sessionsInWindow / days) : 0;\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.accent}>\n {t.stats.title}\n </Text>\n\n <Box marginTop={1} flexDirection=\"column\">\n <Text color={PALETTE.muted}>{t.stats.lifetime}</Text>\n <Box marginTop={1}>\n <Stat label={t.stats.sessions} value={String(sessions.length)} />\n <Stat label={t.stats.words} value={String(totalWords)} />\n <Stat label={t.stats.errors} value={String(totalErrors)} />\n <Stat label={t.stats.wpm} value={String(overallWpm)} accent />\n <Stat label={t.stats.accuracy} value={`${Math.round(overallAcc * 1000) / 10}%`} accent />\n <Stat label={t.stats.streak} value={`${streak}d`} accent />\n </Box>\n </Box>\n\n <Box marginTop={2} flexDirection=\"column\">\n <Text color={PALETTE.muted}>{t.stats.last(days)}</Text>\n <Box marginTop={1} flexDirection=\"column\" borderStyle=\"round\" borderColor={PALETTE.muted} paddingX={2} paddingY={0}>\n <MetricBar\n label={t.stats.bars.speed}\n frac={speedPct}\n valueText={`${agg.avgWpm} wpm`}\n pct={Math.round(speedPct * 100)}\n />\n <MetricBar\n label={t.stats.bars.accuracy}\n frac={accPctFrac}\n valueText={`${agg.avgAccPct}%`}\n pct={Math.round(accPctFrac * 100)}\n />\n <MetricBar\n label={t.stats.bars.sessions}\n frac={sessionsPctFrac}\n valueText={`${agg.sessionsInWindow} / ${days}`}\n pct={Math.round(sessionsPctFrac * 100)}\n />\n </Box>\n </Box>\n\n <Box marginTop={2} flexDirection=\"column\">\n <Text color={PALETTE.muted}>{t.stats.recent}</Text>\n {recent.map((s, i) => (\n <RecentRow key={i} session={s} units={t.stats.recentUnits} />\n ))}\n </Box>\n\n {top.length > 0 && (\n <Box marginTop={2} flexDirection=\"column\">\n <Text color={PALETTE.muted}>{t.stats.topMistakes}</Text>\n {top.map(([word, entry]) => (\n <MistakeRow\n key={word}\n word={word}\n count={entry.count}\n dictIds={entry.dictIds}\n multiSuffix={t.stats.multiDictSuffix}\n />\n ))}\n </Box>\n )}\n\n <Box flexGrow={1} />\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.stats.footer}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction RecentRow({\n session,\n units,\n}: {\n session: SessionRecord;\n units: { words: string; errors: string; wpm: string };\n}) {\n const name = useDictName(session.dictId);\n const display = truncateName(name, 14);\n return (\n <Box>\n <Text color={PALETTE.muted}> {session.ts.replace('T', ' ').slice(0, 16)} </Text>\n <Text color={PALETTE.text}>{display.padEnd(14)}</Text>\n <Text color={PALETTE.muted}>\n {' '}ch{String(session.chapter + 1).padStart(3)} {session.mode.padEnd(9)} {String(session.wordCount).padStart(3)}{units.words} {session.errors}{units.errors} {computeWPM(session)}{units.wpm} {Math.round(accuracy(session) * 1000) / 10}%\n </Text>\n </Box>\n );\n}\n\nfunction MistakeRow({\n word,\n count,\n dictIds,\n multiSuffix,\n}: {\n word: string;\n count: number;\n dictIds: string[];\n multiSuffix: (n: number) => string;\n}) {\n const firstId = dictIds[0] ?? '';\n const firstName = useDictName(firstId);\n const suffix = dictIds.length > 1 ? multiSuffix(dictIds.length - 1) : '';\n return (\n <Box>\n <Text color={PALETTE.error}> {String(count).padStart(3)} </Text>\n <Text color={PALETTE.text}>{word.padEnd(20)}</Text>\n <Text color={PALETTE.muted}>\n {truncateName(firstName, 20)}\n {suffix}\n </Text>\n </Box>\n );\n}\n\nfunction Stat({ label, value, accent = false }: { label: string; value: string; accent?: boolean }) {\n return (\n <Box marginRight={3}>\n <Text color={PALETTE.muted}>{label} </Text>\n <Text bold color={accent ? PALETTE.accent : PALETTE.text}>\n {value}\n </Text>\n </Box>\n );\n}\n\n// Pure layout calculation — extracted for testing without an Ink renderer.\n// OVERHEAD = StatsViewer outer paddingX(4) + inner border(2) + inner paddingX(4) = 10.\n// Inside the inner box we lay out: label | bar | marginLeft | value | pct,\n// fixed widths summing to 24. Bar takes whatever cols remain, clamped 0..60.\nexport function calcMetricBarLayout(cols: number): {\n barWidth: number;\n labelWidth: number;\n valueWidth: number;\n pctWidth: number;\n marginLeft: number;\n} {\n const OVERHEAD = 10;\n const labelWidth = 9;\n const valueWidth = 8;\n const pctWidth = 5;\n const marginLeft = 2;\n const fixed = labelWidth + marginLeft + valueWidth + pctWidth;\n const barWidth = Math.max(0, Math.min(60, cols - OVERHEAD - fixed));\n return { barWidth, labelWidth, valueWidth, pctWidth, marginLeft };\n}\n\nfunction MetricBar({\n label,\n frac,\n valueText,\n pct,\n}: {\n label: string;\n frac: number;\n valueText: string;\n pct: number;\n}) {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n const layout = calcMetricBarLayout(cols);\n const clamped = Math.max(0, Math.min(1, frac));\n const filled = Math.round(layout.barWidth * clamped);\n const empty = layout.barWidth - filled;\n return (\n <Box>\n <Box width={layout.labelWidth}>\n <Text color={PALETTE.muted}>{label}</Text>\n </Box>\n <Text color={PALETTE.accent}>{'━'.repeat(filled)}</Text>\n <Text color={PALETTE.muted}>{'─'.repeat(empty)}</Text>\n <Box width={layout.valueWidth} marginLeft={layout.marginLeft}>\n <Text color={PALETTE.text}>{valueText}</Text>\n </Box>\n <Box width={layout.pctWidth} justifyContent=\"flex-end\">\n <Text color={PALETTE.muted}>{pct}%</Text>\n </Box>\n </Box>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","Box","Text","useInput","useStdout","jsx","jsxs","DAY_WINDOWS","StatsViewer","nav","useNav","t","useStrings","sessions","setSessions","useState","book","setBook","windowIdx","setWindowIdx","useEffect","s","b","loadSessions","loadMistakes","useInput","_input","key","i","Box","Text","PALETTE","days","streak","dailyStreak","totalWords","a","totalErrors","totalMs","firstTryWords","overallWpm","overallAcc","recent","top","topN","agg","windowAggregate","speedPct","accPctFrac","sessionsPctFrac","Stat","MetricBar","RecentRow","word","entry","MistakeRow","session","units","name","useDictName","display","truncateName","computeWPM","accuracy","count","dictIds","multiSuffix","firstId","firstName","suffix","label","value","accent","calcMetricBarLayout","cols","frac","valueText","pct","stdout","useStdout","layout","clamped","filled","empty"]}
|
package/dist/chunk-2MRNI465.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{c as a}from"./chunk-6QICLHIY.js";import{createContext as l,useContext as d,useEffect as p,useState as y}from"react";import{jsx as R}from"react/jsx-runtime";var f=l(null);function v({children:t}){let[r,e]=y(null),[o,n]=y(new Map);return p(()=>{let i=!1;return(async()=>{try{let s=await a();if(i)return;let c=new Map;for(let u of s)c.set(u.id,u);e(s),n(c)}catch{i||(e([]),n(new Map))}})(),()=>{i=!0}},[]),R(f.Provider,{value:{registry:r,byId:o},children:t})}function x(){let t=d(f);if(!t)throw new Error("useRegistry must be used inside RegistryProvider");return t}function C(t){let{byId:r}=x();return t?r.get(t)?.name??t:""}var b=/\x1b\[[0-9;]*m/g;function m(t){return t.replace(b,"")}function g(t){let r=m(t),e=0;for(let o of r){let n=o.codePointAt(0);e+=n>11904&&n<64256?2:1}return e}function D(t,r){if(g(t)<=r)return t;let e="",o=0;for(let n of t){let i=n.codePointAt(0),s=i>11904&&i<64256?2:1;if(o+s>r-1)break;e+=n,o+=s}return e+"\u2026"}export{v as a,C as b,g as c,D as d};
|
|
2
|
-
//# sourceMappingURL=chunk-2MRNI465.js.map
|
package/dist/chunk-GUNCRBXR.js
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{a as R,g as C}from"./chunk-WE6IV5XB.js";import{a as L,b as k}from"./chunk-2GTGXODM.js";import{a as B,b as D,c as w,d as H}from"./chunk-2MRNI465.js";import{a as P,b as T,c as N,d as E,f as a}from"./chunk-QEX27D7F.js";var _="\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H",I="\x1B[?25h\x1B[?1049l",v=!1,$=!1;function O(){if($)return;$=!0;let e=()=>{if(v){try{process.stdout.write(I)}catch{}v=!1}};process.once("exit",e),process.once("SIGINT",()=>{e(),process.exit(130)}),process.once("SIGTERM",()=>{e(),process.exit(143)})}function W(){v||process.stdout.isTTY&&process.env.QWERTY_NO_ALTSCREEN!=="1"&&(O(),process.stdout.write(_),v=!0)}function M(){if(v){try{process.stdout.write(I)}catch{}v=!1}}import{Suspense as ue,lazy as A,useRef as le}from"react";import{Box as pe,Text as de,useApp as me,useInput as fe}from"ink";import{useEffect as Q,useState as X}from"react";import{Box as U,useStdout as Z}from"ink";import{jsx as ee}from"react/jsx-runtime";function z({children:e}){let{stdout:t}=Z(),[s,r]=X(()=>({rows:t?.rows??24,cols:t?.columns??80}));return Q(()=>{W();let o=()=>{r({rows:process.stdout.rows??24,cols:process.stdout.columns??80})};return process.stdout.on("resize",o),()=>{process.stdout.off("resize",o),M()}},[]),ee(U,{width:s.cols,height:s.rows,flexDirection:"column",children:e})}import{createContext as te,useContext as re,useEffect as oe,useState as ne}from"react";import{jsx as ie}from"react/jsx-runtime";var F=te({warning:null,ready:!1});function Y({disabled:e,children:t}){let[s,r]=ne({warning:null,ready:!1});return oe(()=>{let o=!1;return R(e).then(()=>{o||r({warning:C(),ready:!0})}).catch(()=>{o||r({warning:null,ready:!0})}),()=>{o=!0}},[e]),ie(F.Provider,{value:s,children:t})}function q(){return re(F)}import{useState as se}from"react";import{Box as y,Text as m,useApp as ae,useInput as ce}from"ink";import{jsx as h,jsxs as g}from"react/jsx-runtime";function V({cfg:e}){let[t,s]=se(0),{exit:r}=ae(),o=T(),f=q(),l=E(),p=D(e.defaultDict),n=l.mainMenu.items,x=c=>{e.defaultDict?o.navigate({name:"practice",params:{dictId:e.defaultDict,chapterIndex:0,mode:e.defaultMode,stealth:c}}):o.navigate({name:"dict",params:{pickerMode:"choose-then-practice"}})},d=[{key:"p",label:n.practiceLabel,hint:e.defaultDict?n.practiceHintWith(H(p,24)):n.practiceHintNone,run:()=>x(e.stealth==="default")}];(e.stealth==="menu"||e.stealth==="default")&&d.push({key:"b",label:n.stealthLabel,hint:n.stealthHint,run:()=>x(!0)}),d.push({key:"d",label:n.dictLabel,hint:n.dictHint,run:()=>o.navigate({name:"dict"})},{key:"w",label:n.wordLabel,hint:n.wordHint,run:()=>o.navigate({name:"word"})},{key:"s",label:n.statsLabel,hint:n.statsHint,run:()=>o.navigate({name:"stats"})},{key:"c",label:n.configLabel,hint:n.configHint,run:()=>o.navigate({name:"config"})},{key:"q",label:n.quitLabel,hint:n.quitHint,run:()=>r()});let J=Math.max(...d.map(c=>w(c.label)))+4;return ce((c,b)=>{if(b.escape){r();return}if(b.upArrow&&s(u=>(u-1+d.length)%d.length),b.downArrow&&s(u=>(u+1)%d.length),b.return){d[t].run();return}if(c==="?"){o.navigate({name:"help"});return}for(let u of d)if(c===u.key){u.run();return}}),g(y,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",children:[g(y,{children:[h(m,{bold:!0,color:a.accent,children:l.app.title}),g(m,{color:a.muted,children:[" \xB7 ",l.app.subtitle]})]}),h(y,{marginTop:2,flexDirection:"column",children:d.map((c,b)=>{let u=b===t,K=" ".repeat(Math.max(0,J-w(c.label)));return g(y,{children:[h(m,{color:u?a.accent:a.muted,children:u?"\u258C ":" "}),g(m,{color:u?a.accent:a.muted,children:["[",c.key,"]"]}),h(m,{children:" "}),g(m,{bold:u,color:u?a.text:a.muted,children:[c.label,K]}),h(m,{color:a.muted,children:c.hint})]},c.key)})}),h(y,{marginTop:2,children:g(m,{color:a.muted,children:[l.mainMenu.hint," \xB7 ",l.mainMenu.helpHint]})}),f.warning&&h(y,{marginTop:1,children:h(m,{color:a.warning,children:l.audio.noPlayer})})]})}import{jsx as i}from"react/jsx-runtime";var he=A(()=>import("./PracticeScreen-GBCDCFRM.js").then(e=>({default:e.PracticeScreen}))),ge=A(()=>import("./DictBrowser-SZVB5W25.js").then(e=>({default:e.DictBrowser}))),Se=A(()=>import("./ConfigEditor-GXFVIJP3.js").then(e=>({default:e.ConfigEditor}))),xe=A(()=>import("./StatsViewer-IEHZS6GX.js").then(e=>({default:e.StatsViewer}))),be=A(()=>import("./WordLookup-DHZTVUR2.js").then(e=>({default:e.WordLookup}))),we=A(()=>import("./HelpScreen-OUP5G5UG.js").then(e=>({default:e.HelpScreen})));function ve(){return i(pe,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(de,{color:a.muted,children:"\u2026"})})}function nt({initial:e,initialCfg:t,inline:s=!1}){return i(L,{initialCfg:t,children:i(ye,{children:i(B,{children:i(Y,{disabled:!t.sounds.master,children:i(P,{initial:e,children:s?i(j,{inline:!0}):i(z,{children:i(j,{})})})})})})})}function ye({children:e}){let{cfg:t}=k();return i(N,{pref:t.language,children:e})}function Ae(e){if(e.name==="practice"){let t=e.params;return`practice:${t.dictId}:${t.chapterIndex}:${t.mode}:${t.stealth?"s":"n"}`}return e.name}function j({inline:e=!1}){let t=T(),{cfg:s}=k(),{exit:r}=me(),o=le(null);fe((n,x)=>{x.ctrl&&n==="c"&&r()});let f=t.current,l=Ae(f);o.current!==l&&(!e&&process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[H"),o.current=l);let p=(()=>{switch(f.name){case"main":return i(V,{cfg:s});case"practice":return i(he,{params:f.params});case"dict":return i(ge,{params:f.params});case"config":return i(Se,{});case"stats":return i(xe,{});case"word":return i(be,{});case"help":return i(we,{})}})();return i(ue,{fallback:i(ve,{}),children:p})}import S from"chalk";import Te from"boxen";function pt(){M()}function G(e,t){let s=Math.floor(e/1e3),r=Math.floor(s/60),o=s%60;return t==="zh"?r===0?`${o} \u79D2`:`${r} \u5206 ${o} \u79D2`:r===0?`${o}s`:`${r}m ${o}s`}function Me(e){return e>=90?S.green:e<75?S.dim:t=>t}function ke(e,t,s){let r=[],o=[t.report.duration];e.chaptersCompleted===0?o.push(t.report.notPracticed):(o.push(t.report.practiced,t.report.chapters,t.report.words,t.report.accuracy,t.report.wpm),e.newMistakeWords>0&&o.push(t.report.newMistakes));let f=Math.max(...o.map(w)),l=n=>n+" ".repeat(Math.max(0,f-w(n))),p=(n,x)=>`${S.dim(l(n))} ${x}`;if(r.push(p(t.report.duration,G(e.totalDurationMs,s))),e.chaptersCompleted===0)r.push(S.dim(t.report.notPracticed));else{r.push(p(t.report.practiced,G(e.practiceMs,s))),r.push(p(t.report.chapters,String(e.chaptersCompleted))),r.push(p(t.report.words,String(e.wordCount)));let n=Math.round(e.accuracy*1e3)/10;r.push(p(t.report.accuracy,Me(n)(`${n}%`))),r.push(p(t.report.wpm,String(e.wpm))),e.newMistakeWords>0&&r.push(p(t.report.newMistakes,S.red(String(e.newMistakeWords))))}return r.push(""),r.push(S.dim.italic(t.report.farewell)),r}function dt(e,t,s){if(e.startedAt===null&&e.chaptersCompleted===0)return;let r=ke(e,t,s).join(`
|
|
2
|
-
`);console.log(Te(r,{title:S.bold.cyan(t.report.title),titleAlignment:"left",borderStyle:"round",borderColor:"gray",padding:{top:1,bottom:1,left:3,right:3},margin:{top:1,bottom:1,left:2,right:0}}))}export{W as a,nt as b,pt as c,dt as d};
|
|
3
|
-
//# sourceMappingURL=chunk-GUNCRBXR.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/util/altscreen.ts","../src/ui/App.tsx","../src/ui/Fullscreen.tsx","../src/ui/audio-context.tsx","../src/ui/screens/MainMenu.tsx","../src/util/report.ts"],"sourcesContent":["// Shared alt-screen state. Both the command entry point (which writes\n// ENTER before render() to avoid Ink's first frame leaking onto the main\n// screen) and Fullscreen.tsx (which used to write ENTER inside useEffect)\n// route through this module so enter/leave are idempotent.\n\nconst ENTER = '\\x1b[?1049h\\x1b[?25l\\x1b[2J\\x1b[H';\nconst LEAVE = '\\x1b[?25h\\x1b[?1049l';\n\nlet active = false;\nlet signalsRegistered = false;\n\nfunction ensureSignals(): void {\n if (signalsRegistered) return;\n signalsRegistered = true;\n const cleanup = () => {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n };\n process.once('exit', cleanup);\n process.once('SIGINT', () => {\n cleanup();\n process.exit(130);\n });\n process.once('SIGTERM', () => {\n cleanup();\n process.exit(143);\n });\n}\n\nexport function enterAltScreen(): void {\n if (active) return;\n if (!process.stdout.isTTY) return;\n if (process.env.QWERTY_NO_ALTSCREEN === '1') return;\n ensureSignals();\n process.stdout.write(ENTER);\n active = true;\n}\n\nexport function leaveAltScreen(): void {\n if (!active) return;\n try {\n process.stdout.write(LEAVE);\n } catch {\n // stream may be closed\n }\n active = false;\n}\n\nexport function isAltScreenActive(): boolean {\n return active;\n}\n","import { Suspense, lazy, useRef, type ReactNode } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { NavProvider, useNav, type ScreenFrame } from './nav.js';\nimport { Fullscreen } from './Fullscreen.js';\nimport { AudioStatusProvider } from './audio-context.js';\nimport { AppStateProvider, useAppState } from './app-state.js';\nimport { StringsProvider } from '../i18n/context.js';\nimport { RegistryProvider } from './registry-context.js';\nimport { MainMenu } from './screens/MainMenu.js';\nimport { PALETTE } from './components/BigWord.js';\nimport type { Config } from '../infra/config-store.js';\n\n// MainMenu stays eager — it's the initial screen for `qwerty` (no args) and\n// must render instantly. All other screens are lazy so their module graphs\n// (DictBrowser pulls registry helpers, StatsViewer pulls stats domain, etc.)\n// only load when the user navigates to them.\nconst PracticeScreen = lazy(() =>\n import('./screens/PracticeScreen.js').then((m) => ({ default: m.PracticeScreen })),\n);\nconst DictBrowser = lazy(() =>\n import('./screens/DictBrowser.js').then((m) => ({ default: m.DictBrowser })),\n);\nconst ConfigEditor = lazy(() =>\n import('./screens/ConfigEditor.js').then((m) => ({ default: m.ConfigEditor })),\n);\nconst StatsViewer = lazy(() =>\n import('./screens/StatsViewer.js').then((m) => ({ default: m.StatsViewer })),\n);\nconst WordLookup = lazy(() =>\n import('./screens/WordLookup.js').then((m) => ({ default: m.WordLookup })),\n);\nconst HelpScreen = lazy(() =>\n import('./screens/HelpScreen.js').then((m) => ({ default: m.HelpScreen })),\n);\n\nfunction LazyFallback() {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>…</Text>\n </Box>\n );\n}\n\nexport function App({\n initial,\n initialCfg,\n inline = false,\n}: {\n initial: ScreenFrame;\n initialCfg: Config;\n inline?: boolean;\n}) {\n return (\n <AppStateProvider initialCfg={initialCfg}>\n <LangBridge>\n <RegistryProvider>\n <AudioStatusProvider disabled={!initialCfg.sounds.master}>\n <NavProvider initial={initial}>\n {inline ? <Router inline /> : <Fullscreen><Router /></Fullscreen>}\n </NavProvider>\n </AudioStatusProvider>\n </RegistryProvider>\n </LangBridge>\n </AppStateProvider>\n );\n}\n\nfunction LangBridge({ children }: { children: ReactNode }) {\n const { cfg } = useAppState();\n return <StringsProvider pref={cfg.language}>{children}</StringsProvider>;\n}\n\nfunction screenKey(frame: ScreenFrame): string {\n if (frame.name === 'practice') {\n const p = frame.params;\n return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? 's' : 'n'}`;\n }\n return frame.name;\n}\n\nfunction Router({ inline = false }: { inline?: boolean }) {\n const nav = useNav();\n const { cfg } = useAppState();\n const { exit } = useApp();\n const lastKeyRef = useRef<string | null>(null);\n\n useInput((input, key) => {\n if (key.ctrl && input === 'c') exit();\n });\n\n const frame = nav.current;\n const key = screenKey(frame);\n if (lastKeyRef.current !== key) {\n if (!inline && process.stdout.isTTY) process.stdout.write('\\x1b[2J\\x1b[H');\n lastKeyRef.current = key;\n }\n\n const screen = (() => {\n switch (frame.name) {\n case 'main':\n return <MainMenu cfg={cfg} />;\n case 'practice':\n return <PracticeScreen params={frame.params} />;\n case 'dict':\n return <DictBrowser params={frame.params} />;\n case 'config':\n return <ConfigEditor />;\n case 'stats':\n return <StatsViewer />;\n case 'word':\n return <WordLookup />;\n case 'help':\n return <HelpScreen />;\n }\n })();\n\n return <Suspense fallback={<LazyFallback />}>{screen}</Suspense>;\n}\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { Box, useStdout } from 'ink';\nimport { enterAltScreen, leaveAltScreen } from '../util/altscreen.js';\n\nexport function Fullscreen({ children }: { children: ReactNode }) {\n const { stdout } = useStdout();\n const [size, setSize] = useState(() => ({\n rows: stdout?.rows ?? 24,\n cols: stdout?.columns ?? 80,\n }));\n\n useEffect(() => {\n // enterAltScreen is idempotent — if the command entry already wrote the\n // ENTER sequence pre-render (the normal path for menu / non-stealth\n // practice), this is a no-op. Acts as a safety net for any code path\n // that mounts the App without pre-render alt-screen setup.\n enterAltScreen();\n\n const onResize = () => {\n setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });\n };\n process.stdout.on('resize', onResize);\n\n return () => {\n process.stdout.off('resize', onResize);\n leaveAltScreen();\n };\n }, []);\n\n return (\n <Box width={size.cols} height={size.rows} flexDirection=\"column\">\n {children}\n </Box>\n );\n}\n","import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';\nimport { initAudio, audioWarning } from '../infra/audio.js';\n\ntype AudioStatus = { warning: string | null; ready: boolean };\n\nconst AudioStatusContext = createContext<AudioStatus>({ warning: null, ready: false });\n\nexport function AudioStatusProvider({\n disabled,\n children,\n}: {\n disabled: boolean;\n children: ReactNode;\n}) {\n const [status, setStatus] = useState<AudioStatus>({ warning: null, ready: false });\n\n useEffect(() => {\n let cancelled = false;\n initAudio(disabled)\n .then(() => {\n if (cancelled) return;\n setStatus({ warning: audioWarning(), ready: true });\n })\n .catch(() => {\n if (cancelled) return;\n setStatus({ warning: null, ready: true });\n });\n return () => {\n cancelled = true;\n };\n }, [disabled]);\n\n return <AudioStatusContext.Provider value={status}>{children}</AudioStatusContext.Provider>;\n}\n\nexport function useAudioStatus(): AudioStatus {\n return useContext(AudioStatusContext);\n}\n","import { useState } from 'react';\nimport { Box, Text, useApp, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { visibleWidth } from '../../util/text.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport type { Config } from '../../infra/config-store.js';\n\ntype Item = { key: string; label: string; hint: string; run: () => void };\n\nexport function MainMenu({ cfg }: { cfg: Config }) {\n const [selected, setSelected] = useState(0);\n const { exit } = useApp();\n const nav = useNav();\n const audio = useAudioStatus();\n const t = useStrings();\n const defaultDictName = useDictName(cfg.defaultDict);\n\n const m = t.mainMenu.items;\n const startPractice = (stealth: boolean) => {\n if (cfg.defaultDict) {\n nav.navigate({\n name: 'practice',\n params: {\n dictId: cfg.defaultDict,\n chapterIndex: 0,\n mode: cfg.defaultMode,\n stealth,\n },\n });\n } else {\n nav.navigate({ name: 'dict', params: { pickerMode: 'choose-then-practice' } });\n }\n };\n\n const items: Item[] = [\n {\n key: 'p',\n label: m.practiceLabel,\n hint: cfg.defaultDict\n ? m.practiceHintWith(truncateName(defaultDictName, 24))\n : m.practiceHintNone,\n run: () => startPractice(cfg.stealth === 'default'),\n },\n ];\n\n if (cfg.stealth === 'menu' || cfg.stealth === 'default') {\n items.push({\n key: 'b',\n label: m.stealthLabel,\n hint: m.stealthHint,\n run: () => startPractice(true),\n });\n }\n\n items.push(\n { key: 'd', label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: 'dict' }) },\n { key: 'w', label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: 'word' }) },\n { key: 's', label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: 'stats' }) },\n { key: 'c', label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: 'config' }) },\n { key: 'q', label: m.quitLabel, hint: m.quitHint, run: () => exit() },\n );\n\n const labelW = Math.max(...items.map((it) => visibleWidth(it.label))) + 4;\n\n useInput((input, key) => {\n if (key.escape) {\n exit();\n return;\n }\n if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);\n if (key.downArrow) setSelected((i) => (i + 1) % items.length);\n if (key.return) {\n items[selected]!.run();\n return;\n }\n if (input === '?') {\n nav.navigate({ name: 'help' });\n return;\n }\n for (const it of items) {\n if (input === it.key) {\n it.run();\n return;\n }\n }\n });\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\">\n <Box>\n <Text bold color={PALETTE.accent}>\n {t.app.title}\n </Text>\n <Text color={PALETTE.muted}> · {t.app.subtitle}</Text>\n </Box>\n\n <Box marginTop={2} flexDirection=\"column\">\n {items.map((it, i) => {\n const active = i === selected;\n const pad = ' '.repeat(Math.max(0, labelW - visibleWidth(it.label)));\n return (\n <Box key={it.key}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>[{it.key}]</Text>\n <Text>{' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {it.label}\n {pad}\n </Text>\n <Text color={PALETTE.muted}>{it.hint}</Text>\n </Box>\n );\n })}\n </Box>\n\n <Box marginTop={2}>\n <Text color={PALETTE.muted}>\n {t.mainMenu.hint}\n {' · '}\n {t.mainMenu.helpHint}\n </Text>\n </Box>\n\n {audio.warning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.audio.noPlayer}</Text>\n </Box>\n )}\n </Box>\n );\n}\n","import chalk from 'chalk';\nimport boxen from 'boxen';\nimport type { SessionReport } from '../infra/session-tracker.js';\nimport type { Strings } from '../i18n/strings.js';\nimport { leaveAltScreen } from './altscreen.js';\nimport { visibleWidth } from './text.js';\n\nexport function ensureMainScreen(): void {\n leaveAltScreen();\n}\n\nfunction fmtDuration(ms: number, lang: 'zh' | 'en'): string {\n const total = Math.floor(ms / 1000);\n const m = Math.floor(total / 60);\n const s = total % 60;\n if (lang === 'zh') {\n if (m === 0) return `${s} 秒`;\n return `${m} 分 ${s} 秒`;\n }\n if (m === 0) return `${s}s`;\n return `${m}m ${s}s`;\n}\n\nfunction accColor(pct: number): (s: string) => string {\n if (pct >= 90) return chalk.green;\n if (pct < 75) return chalk.dim;\n return (s) => s;\n}\n\nfunction buildLines(r: SessionReport, t: Strings, lang: 'zh' | 'en'): string[] {\n const lines: string[] = [];\n\n // Align labels in a single visual column. Use the longest label's\n // visible width as the target (CJK counted at 2 cols by visibleWidth).\n const allLabels: string[] = [t.report.duration];\n if (r.chaptersCompleted === 0) {\n allLabels.push(t.report.notPracticed);\n } else {\n allLabels.push(\n t.report.practiced,\n t.report.chapters,\n t.report.words,\n t.report.accuracy,\n t.report.wpm,\n );\n if (r.newMistakeWords > 0) allLabels.push(t.report.newMistakes);\n }\n const labelW = Math.max(...allLabels.map(visibleWidth));\n const padLabel = (s: string) =>\n s + ' '.repeat(Math.max(0, labelW - visibleWidth(s)));\n const row = (label: string, value: string) =>\n `${chalk.dim(padLabel(label))} ${value}`;\n\n lines.push(row(t.report.duration, fmtDuration(r.totalDurationMs, lang)));\n\n if (r.chaptersCompleted === 0) {\n lines.push(chalk.dim(t.report.notPracticed));\n } else {\n lines.push(row(t.report.practiced, fmtDuration(r.practiceMs, lang)));\n lines.push(row(t.report.chapters, String(r.chaptersCompleted)));\n lines.push(row(t.report.words, String(r.wordCount)));\n const accPct = Math.round(r.accuracy * 1000) / 10;\n lines.push(row(t.report.accuracy, accColor(accPct)(`${accPct}%`)));\n lines.push(row(t.report.wpm, String(r.wpm)));\n if (r.newMistakeWords > 0) {\n lines.push(row(t.report.newMistakes, chalk.red(String(r.newMistakeWords))));\n }\n }\n\n lines.push('');\n lines.push(chalk.dim.italic(t.report.farewell));\n return lines;\n}\n\nexport function printSessionReport(r: SessionReport, t: Strings, lang: 'zh' | 'en'): void {\n if (r.startedAt === null && r.chaptersCompleted === 0) return;\n const content = buildLines(r, t, lang).join('\\n');\n console.log(\n boxen(content, {\n title: chalk.bold.cyan(t.report.title),\n titleAlignment: 'left',\n borderStyle: 'round',\n borderColor: 'gray',\n padding: { top: 1, bottom: 1, left: 3, right: 3 },\n margin: { top: 1, bottom: 1, left: 2, right: 0 },\n }),\n );\n}\n"],"mappings":"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","names":["ENTER","LEAVE","active","signalsRegistered","ensureSignals","cleanup","enterAltScreen","leaveAltScreen","Suspense","lazy","useRef","Box","Text","useApp","useInput","useEffect","useState","Box","useStdout","jsx","Fullscreen","children","stdout","useStdout","size","setSize","useState","useEffect","enterAltScreen","onResize","leaveAltScreen","Box","createContext","useContext","useEffect","useState","jsx","AudioStatusContext","createContext","AudioStatusProvider","disabled","children","status","setStatus","useState","useEffect","cancelled","initAudio","audioWarning","useAudioStatus","useContext","useState","Box","Text","useApp","useInput","jsx","jsxs","MainMenu","cfg","selected","setSelected","useState","exit","useApp","nav","useNav","audio","useAudioStatus","t","useStrings","defaultDictName","useDictName","m","startPractice","stealth","items","truncateName","labelW","it","visibleWidth","useInput","input","key","i","Box","Text","PALETTE","active","pad","jsx","PracticeScreen","lazy","m","DictBrowser","ConfigEditor","StatsViewer","WordLookup","HelpScreen","LazyFallback","Box","Text","PALETTE","App","initial","initialCfg","inline","AppStateProvider","LangBridge","RegistryProvider","AudioStatusProvider","NavProvider","Router","Fullscreen","children","cfg","useAppState","StringsProvider","screenKey","frame","p","nav","useNav","exit","useApp","lastKeyRef","useRef","useInput","input","key","screen","MainMenu","Suspense","chalk","boxen","ensureMainScreen","leaveAltScreen","fmtDuration","ms","lang","total","m","s","accColor","pct","chalk","buildLines","r","lines","allLabels","labelW","visibleWidth","padLabel","row","label","value","accPct","printSessionReport","content","boxen"]}
|