qwerty-cli 0.0.1-alpha.19 → 0.0.1-alpha.21
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-YG5K2WT2.js +2 -0
- package/dist/{ConfigEditor-YYZQ7TUJ.js.map → ConfigEditor-YG5K2WT2.js.map} +1 -1
- package/dist/PracticeScreen-BRPEUQJ6.js +2 -0
- package/dist/PracticeScreen-BRPEUQJ6.js.map +1 -0
- package/dist/StatsViewer-43ATH3OG.js +2 -0
- package/dist/{StatsViewer-JIFWFPDL.js.map → StatsViewer-43ATH3OG.js.map} +1 -1
- package/dist/WordLookup-LQMQVORJ.js +2 -0
- package/dist/WordLookup-LQMQVORJ.js.map +1 -0
- package/dist/chunk-MHLVBOJU.js +3 -0
- package/dist/chunk-MHLVBOJU.js.map +1 -0
- package/dist/chunk-UEJCQKZ2.js +2 -0
- package/dist/chunk-UEJCQKZ2.js.map +1 -0
- package/dist/chunk-WRO5XX35.js +2 -0
- package/dist/chunk-WRO5XX35.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/menu.impl-6DPHV36B.js +2 -0
- package/dist/{menu.impl-ZUAMXKFD.js.map → menu.impl-6DPHV36B.js.map} +1 -1
- package/dist/practice.impl-HILWI6GI.js +2 -0
- package/dist/{practice.impl-2PPCBBK3.js.map → practice.impl-HILWI6GI.js.map} +1 -1
- package/dist/word.impl-VVSIGCKS.js +2 -0
- package/dist/word.impl-VVSIGCKS.js.map +1 -0
- package/package.json +1 -1
- package/dist/ConfigEditor-YYZQ7TUJ.js +0 -2
- package/dist/PracticeScreen-PXPBME6H.js +0 -2
- package/dist/PracticeScreen-PXPBME6H.js.map +0 -1
- package/dist/StatsViewer-JIFWFPDL.js +0 -2
- package/dist/WordLookup-6JFXQYSQ.js +0 -2
- package/dist/WordLookup-6JFXQYSQ.js.map +0 -1
- package/dist/chunk-QG7ZTS2G.js +0 -2
- package/dist/chunk-QG7ZTS2G.js.map +0 -1
- package/dist/chunk-RR77GWZ3.js +0 -3
- package/dist/chunk-RR77GWZ3.js.map +0 -1
- package/dist/menu.impl-ZUAMXKFD.js +0 -2
- package/dist/practice.impl-2PPCBBK3.js +0 -2
- package/dist/word.impl-EJNGBD7U.js +0 -2
- package/dist/word.impl-EJNGBD7U.js.map +0 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{b as P}from"./chunk-MFGIEKBU.js";import{d as E}from"./chunk-VTIB5Q36.js";import{b as $,c as C}from"./chunk-UEJCQKZ2.js";import{a as B}from"./chunk-WRO5XX35.js";import{b as F,d as M,f as l}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as k}from"react";import{Box as g,Text as c,useInput as V}from"ink";import{jsx as d,jsxs as S}from"react/jsx-runtime";var f=[{kind:"dictRef",path:"defaultDict",labelKey:"defaultDict"},{kind:"enum",path:"defaultMode",labelKey:"defaultMode",options:["order","dictation","review","random","loop"]},{kind:"enum",path:"accent",labelKey:"accent",options:["us","uk"]},{kind:"enum",path:"language",labelKey:"language",options:["auto","zh","en"]},{kind:"enum",path:"mirror",labelKey:"mirror",options:["jsdelivr","github"]},{kind:"enum",path:"stealth",labelKey:"stealth",options:["off","menu","default"]},{kind:"enum",path:"wordDisplay",labelKey:"wordDisplay",options:["auto","huge","standard"]},{kind:"int",path:"chapterSize",labelKey:"chapterSize",min:1,max:200},{kind:"bool",path:"autoplayPronunciation",labelKey:"autoplayPronunciation"},{kind:"bool",path:"sounds.master",labelKey:"soundsMaster"},{kind:"bool",path:"sounds.keystroke",labelKey:"soundsKeystroke"},{kind:"bool",path:"sounds.feedback",labelKey:"soundsFeedback"},{kind:"enum",path:"sounds.pronunciationRate",labelKey:"soundsPronunciationRate",options:["0.5","0.75","1","1.25","1.5"]},{kind:"enum",path:"sounds.pronunciationSource",labelKey:"soundsPronunciationSource",options:["youdao","dictapi"]}];function N(a,e){return e.split(".").reduce((o,s)=>{if(o&&typeof o=="object")return o[s]},a)}function U(){let a=F(),{cfg:e,setCfg:o}=P(),s=M(),y=$(e.defaultDict),[x,K]=k(0),[m,h]=k(!1),[w,p]=k(""),[v,u]=k(null),r=f[x],D=N(e,r.path),b=async i=>{try{let n=E(e,r.path,i);await o(n),h(!1),u(null)}catch(n){u(n.message)}};V((i,n)=>{if(m&&r.kind==="string"){if(n.escape){h(!1),u(null);return}if(n.return){b(w);return}if(n.backspace||n.delete){p(t=>t.slice(0,-1));return}i&&!n.ctrl&&!n.meta&&p(t=>t+i);return}if(m&&r.kind==="int"){if(n.escape){h(!1),u(null);return}if(n.return){b(w);return}if(n.backspace||n.delete){p(t=>t.slice(0,-1));return}/^[0-9]$/.test(i)&&p(t=>t+i);return}if(n.escape){a.back();return}if(n.upArrow){K(t=>(t-1+f.length)%f.length),u(null);return}if(n.downArrow){K(t=>(t+1)%f.length),u(null);return}if(r.kind==="bool"&&(i===" "||n.return)){b(D?"false":"true");return}if(r.kind==="enum"&&(n.leftArrow||n.rightArrow)){let t=r.options.indexOf(String(D)),T=n.rightArrow?1:-1,R=r.options[(t+T+r.options.length)%r.options.length];b(R);return}if(r.kind==="dictRef"&&n.return){a.navigate({name:"dict",params:{pickerMode:"set-default"}});return}(r.kind==="string"||r.kind==="int")&&n.return&&(p(String(D??"")),h(!0),u(null))});let z=Math.max(...f.map(i=>B(s.config.fields[i.labelKey])))+4;return S(g,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[d(c,{bold:!0,color:l.accent,children:s.config.title}),d(g,{marginTop:1,flexDirection:"column",flexGrow:1,children:f.map((i,n)=>{let t=n===x,T=N(e,i.path),R=j(i,T,t&&m?w:null,s,i.path==="defaultDict"?y:""),A=s.config.fields[i.labelKey],I=" ".repeat(Math.max(0,z-B(A)));return S(g,{children:[d(c,{color:t?l.accent:l.muted,children:t?"\u258C ":" "}),S(c,{bold:t,color:t?l.text:l.muted,children:[A,I]}),d(c,{color:t?l.accent:l.muted,children:R})]},i.path)})}),v&&d(g,{marginTop:1,children:S(c,{color:l.error,children:["! ",v]})}),d(g,{marginTop:1,children:d(c,{color:l.muted,children:L(r,m,s)})})]})}function j(a,e,o,s,y){return o!==null?`${o}_`:a.kind==="bool"?e?`\u2713 ${s.common.on}`:`\u2717 ${s.common.off}`:a.kind==="dictRef"?e?C(y||String(e),24):"\u2014":a.kind==="enum"?`< ${s.config.enumValues[a.labelKey]?.[String(e)]??String(e)} >`:String(e??"")}function L(a,e,o){return e?o.config.hints.editing:a.kind==="bool"?o.config.hints.bool:a.kind==="enum"?o.config.hints.enum:a.kind==="dictRef"?o.config.hints.dictRef:o.config.hints.stringOrInt}export{U as ConfigEditor};
|
|
2
|
+
//# sourceMappingURL=ConfigEditor-YG5K2WT2.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ui/screens/ConfigEditor.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { setByPath, type Config } from '../../infra/config-store.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { visibleWidth } from '../../util/text.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport type { Strings } from '../../i18n/strings.js';\n\ntype FieldSpec =\n | { kind: 'enum'; path: string; labelKey: keyof Strings['config']['fields']; options: string[] }\n | { kind: 'bool'; path: string; labelKey: keyof Strings['config']['fields'] }\n | { kind: 'int'; path: string; labelKey: keyof Strings['config']['fields']; min: number; max: number }\n | { kind: 'string'; path: string; labelKey: keyof Strings['config']['fields'] }\n | { kind: 'dictRef'; path: 'defaultDict'; labelKey: keyof Strings['config']['fields'] };\n\nconst FIELDS: FieldSpec[] = [\n { kind: 'dictRef', path: 'defaultDict', labelKey: 'defaultDict' },\n { kind: 'enum', path: 'defaultMode', labelKey: 'defaultMode', options: ['order', 'dictation', 'review', 'random', 'loop'] },\n { kind: 'enum', path: 'accent', labelKey: 'accent', options: ['us', 'uk'] },\n { kind: 'enum', path: 'language', labelKey: 'language', options: ['auto', 'zh', 'en'] },\n { kind: 'enum', path: 'mirror', labelKey: 'mirror', options: ['jsdelivr', 'github'] },\n { kind: 'enum', path: 'stealth', labelKey: 'stealth', options: ['off', 'menu', 'default'] },\n { kind: 'enum', path: 'wordDisplay', labelKey: 'wordDisplay', options: ['auto', 'huge', 'standard'] },\n { kind: 'int', path: 'chapterSize', labelKey: 'chapterSize', min: 1, max: 200 },\n { kind: 'bool', path: 'autoplayPronunciation', labelKey: 'autoplayPronunciation' },\n { kind: 'bool', path: 'sounds.master', labelKey: 'soundsMaster' },\n { kind: 'bool', path: 'sounds.keystroke', labelKey: 'soundsKeystroke' },\n { kind: 'bool', path: 'sounds.feedback', labelKey: 'soundsFeedback' },\n {\n kind: 'enum',\n path: 'sounds.pronunciationRate',\n labelKey: 'soundsPronunciationRate',\n options: ['0.5', '0.75', '1', '1.25', '1.5'],\n },\n {\n kind: 'enum',\n path: 'sounds.pronunciationSource',\n labelKey: 'soundsPronunciationSource',\n options: ['youdao', 'dictapi'],\n },\n];\n\nfunction getByPath(cfg: Config, path: string): unknown {\n return path.split('.').reduce<unknown>((acc, k) => {\n if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[k];\n return undefined;\n }, cfg);\n}\n\nexport function ConfigEditor() {\n const nav = useNav();\n const { cfg, setCfg } = useAppState();\n const t = useStrings();\n const defaultDictName = useDictName(cfg.defaultDict);\n const [selected, setSelected] = useState(0);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const field = FIELDS[selected]!;\n const currentValue = getByPath(cfg, field.path);\n\n const commit = async (raw: string) => {\n try {\n const next = setByPath(cfg, field.path, raw);\n await setCfg(next);\n setEditing(false);\n setError(null);\n } catch (err) {\n setError((err as Error).message);\n }\n };\n\n useInput((input, key) => {\n if (editing && field.kind === 'string') {\n if (key.escape) {\n setEditing(false);\n setError(null);\n return;\n }\n if (key.return) {\n void commit(draft);\n return;\n }\n if (key.backspace || key.delete) {\n setDraft((d) => d.slice(0, -1));\n return;\n }\n if (input && !key.ctrl && !key.meta) setDraft((d) => d + input);\n return;\n }\n if (editing && field.kind === 'int') {\n if (key.escape) {\n setEditing(false);\n setError(null);\n return;\n }\n if (key.return) {\n void commit(draft);\n return;\n }\n if (key.backspace || key.delete) {\n setDraft((d) => d.slice(0, -1));\n return;\n }\n if (/^[0-9]$/.test(input)) setDraft((d) => d + input);\n return;\n }\n\n if (key.escape) {\n nav.back();\n return;\n }\n if (key.upArrow) {\n setSelected((i) => (i - 1 + FIELDS.length) % FIELDS.length);\n setError(null);\n return;\n }\n if (key.downArrow) {\n setSelected((i) => (i + 1) % FIELDS.length);\n setError(null);\n return;\n }\n if (field.kind === 'bool' && (input === ' ' || key.return)) {\n void commit(currentValue ? 'false' : 'true');\n return;\n }\n if (field.kind === 'enum' && (key.leftArrow || key.rightArrow)) {\n const idx = field.options.indexOf(String(currentValue));\n const delta = key.rightArrow ? 1 : -1;\n const next = field.options[(idx + delta + field.options.length) % field.options.length]!;\n void commit(next);\n return;\n }\n if (field.kind === 'dictRef' && key.return) {\n nav.navigate({ name: 'dict', params: { pickerMode: 'set-default' } });\n return;\n }\n if ((field.kind === 'string' || field.kind === 'int') && key.return) {\n setDraft(String(currentValue ?? ''));\n setEditing(true);\n setError(null);\n }\n });\n\n const labelW = Math.max(...FIELDS.map((f) => visibleWidth(t.config.fields[f.labelKey]))) + 4;\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.accent}>\n {t.config.title}\n </Text>\n\n <Box marginTop={1} flexDirection=\"column\" flexGrow={1}>\n {FIELDS.map((f, i) => {\n const active = i === selected;\n const value = getByPath(cfg, f.path);\n const display = renderValue(\n f,\n value,\n active && editing ? draft : null,\n t,\n f.path === 'defaultDict' ? defaultDictName : '',\n );\n const label = t.config.fields[f.labelKey];\n const pad = ' '.repeat(Math.max(0, labelW - visibleWidth(label)));\n return (\n <Box key={f.path}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {label}\n {pad}\n </Text>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{display}</Text>\n </Box>\n );\n })}\n </Box>\n\n {error && (\n <Box marginTop={1}>\n <Text color={PALETTE.error}>! {error}</Text>\n </Box>\n )}\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{hintFor(field, editing, t)}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction renderValue(\n field: FieldSpec,\n value: unknown,\n draft: string | null,\n t: Strings,\n dictDisplayName: string,\n): string {\n if (draft !== null) return `${draft}_`;\n if (field.kind === 'bool') return value ? `✓ ${t.common.on}` : `✗ ${t.common.off}`;\n if (field.kind === 'dictRef') {\n if (!value) return '—';\n return truncateName(dictDisplayName || String(value), 24);\n }\n if (field.kind === 'enum') {\n const map = (t.config.enumValues as Record<string, Record<string, string> | undefined>)[\n field.labelKey\n ];\n const label = map?.[String(value)] ?? String(value);\n return `< ${label} >`;\n }\n return String(value ?? '');\n}\n\nfunction hintFor(field: FieldSpec, editing: boolean, t: Strings): string {\n if (editing) return t.config.hints.editing;\n if (field.kind === 'bool') return t.config.hints.bool;\n if (field.kind === 'enum') return t.config.hints.enum;\n if (field.kind === 'dictRef') return t.config.hints.dictRef;\n return t.config.hints.stringOrInt;\n}\n"],"mappings":"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","names":["useState","Box","Text","useInput","jsx","jsxs","FIELDS","getByPath","cfg","path","acc","k","ConfigEditor","nav","useNav","setCfg","useAppState","t","useStrings","defaultDictName","useDictName","selected","setSelected","useState","editing","setEditing","draft","setDraft","error","setError","field","currentValue","commit","raw","next","setByPath","err","useInput","input","key","d","i","idx","delta","labelW","f","visibleWidth","Box","Text","PALETTE","active","value","display","renderValue","label","pad","hintFor","dictDisplayName","truncateName"]}
|
|
1
|
+
{"version":3,"sources":["../src/ui/screens/ConfigEditor.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport { useNav } from '../nav.js';\nimport { useAppState } from '../app-state.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { setByPath, type Config } from '../../infra/config-store.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { visibleWidth } from '../../util/text.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport type { Strings } from '../../i18n/strings.js';\n\ntype FieldSpec =\n | { kind: 'enum'; path: string; labelKey: keyof Strings['config']['fields']; options: string[] }\n | { kind: 'bool'; path: string; labelKey: keyof Strings['config']['fields'] }\n | { kind: 'int'; path: string; labelKey: keyof Strings['config']['fields']; min: number; max: number }\n | { kind: 'string'; path: string; labelKey: keyof Strings['config']['fields'] }\n | { kind: 'dictRef'; path: 'defaultDict'; labelKey: keyof Strings['config']['fields'] };\n\nconst FIELDS: FieldSpec[] = [\n { kind: 'dictRef', path: 'defaultDict', labelKey: 'defaultDict' },\n { kind: 'enum', path: 'defaultMode', labelKey: 'defaultMode', options: ['order', 'dictation', 'review', 'random', 'loop'] },\n { kind: 'enum', path: 'accent', labelKey: 'accent', options: ['us', 'uk'] },\n { kind: 'enum', path: 'language', labelKey: 'language', options: ['auto', 'zh', 'en'] },\n { kind: 'enum', path: 'mirror', labelKey: 'mirror', options: ['jsdelivr', 'github'] },\n { kind: 'enum', path: 'stealth', labelKey: 'stealth', options: ['off', 'menu', 'default'] },\n { kind: 'enum', path: 'wordDisplay', labelKey: 'wordDisplay', options: ['auto', 'huge', 'standard'] },\n { kind: 'int', path: 'chapterSize', labelKey: 'chapterSize', min: 1, max: 200 },\n { kind: 'bool', path: 'autoplayPronunciation', labelKey: 'autoplayPronunciation' },\n { kind: 'bool', path: 'sounds.master', labelKey: 'soundsMaster' },\n { kind: 'bool', path: 'sounds.keystroke', labelKey: 'soundsKeystroke' },\n { kind: 'bool', path: 'sounds.feedback', labelKey: 'soundsFeedback' },\n {\n kind: 'enum',\n path: 'sounds.pronunciationRate',\n labelKey: 'soundsPronunciationRate',\n options: ['0.5', '0.75', '1', '1.25', '1.5'],\n },\n {\n kind: 'enum',\n path: 'sounds.pronunciationSource',\n labelKey: 'soundsPronunciationSource',\n options: ['youdao', 'dictapi'],\n },\n];\n\nfunction getByPath(cfg: Config, path: string): unknown {\n return path.split('.').reduce<unknown>((acc, k) => {\n if (acc && typeof acc === 'object') return (acc as Record<string, unknown>)[k];\n return undefined;\n }, cfg);\n}\n\nexport function ConfigEditor() {\n const nav = useNav();\n const { cfg, setCfg } = useAppState();\n const t = useStrings();\n const defaultDictName = useDictName(cfg.defaultDict);\n const [selected, setSelected] = useState(0);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const field = FIELDS[selected]!;\n const currentValue = getByPath(cfg, field.path);\n\n const commit = async (raw: string) => {\n try {\n const next = setByPath(cfg, field.path, raw);\n await setCfg(next);\n setEditing(false);\n setError(null);\n } catch (err) {\n setError((err as Error).message);\n }\n };\n\n useInput((input, key) => {\n if (editing && field.kind === 'string') {\n if (key.escape) {\n setEditing(false);\n setError(null);\n return;\n }\n if (key.return) {\n void commit(draft);\n return;\n }\n if (key.backspace || key.delete) {\n setDraft((d) => d.slice(0, -1));\n return;\n }\n if (input && !key.ctrl && !key.meta) setDraft((d) => d + input);\n return;\n }\n if (editing && field.kind === 'int') {\n if (key.escape) {\n setEditing(false);\n setError(null);\n return;\n }\n if (key.return) {\n void commit(draft);\n return;\n }\n if (key.backspace || key.delete) {\n setDraft((d) => d.slice(0, -1));\n return;\n }\n if (/^[0-9]$/.test(input)) setDraft((d) => d + input);\n return;\n }\n\n if (key.escape) {\n nav.back();\n return;\n }\n if (key.upArrow) {\n setSelected((i) => (i - 1 + FIELDS.length) % FIELDS.length);\n setError(null);\n return;\n }\n if (key.downArrow) {\n setSelected((i) => (i + 1) % FIELDS.length);\n setError(null);\n return;\n }\n if (field.kind === 'bool' && (input === ' ' || key.return)) {\n void commit(currentValue ? 'false' : 'true');\n return;\n }\n if (field.kind === 'enum' && (key.leftArrow || key.rightArrow)) {\n const idx = field.options.indexOf(String(currentValue));\n const delta = key.rightArrow ? 1 : -1;\n const next = field.options[(idx + delta + field.options.length) % field.options.length]!;\n void commit(next);\n return;\n }\n if (field.kind === 'dictRef' && key.return) {\n nav.navigate({ name: 'dict', params: { pickerMode: 'set-default' } });\n return;\n }\n if ((field.kind === 'string' || field.kind === 'int') && key.return) {\n setDraft(String(currentValue ?? ''));\n setEditing(true);\n setError(null);\n }\n });\n\n const labelW = Math.max(...FIELDS.map((f) => visibleWidth(t.config.fields[f.labelKey]))) + 4;\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <Text bold color={PALETTE.accent}>\n {t.config.title}\n </Text>\n\n <Box marginTop={1} flexDirection=\"column\" flexGrow={1}>\n {FIELDS.map((f, i) => {\n const active = i === selected;\n const value = getByPath(cfg, f.path);\n const display = renderValue(\n f,\n value,\n active && editing ? draft : null,\n t,\n f.path === 'defaultDict' ? defaultDictName : '',\n );\n const label = t.config.fields[f.labelKey];\n const pad = ' '.repeat(Math.max(0, labelW - visibleWidth(label)));\n return (\n <Box key={f.path}>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {label}\n {pad}\n </Text>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{display}</Text>\n </Box>\n );\n })}\n </Box>\n\n {error && (\n <Box marginTop={1}>\n <Text color={PALETTE.error}>! {error}</Text>\n </Box>\n )}\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{hintFor(field, editing, t)}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction renderValue(\n field: FieldSpec,\n value: unknown,\n draft: string | null,\n t: Strings,\n dictDisplayName: string,\n): string {\n if (draft !== null) return `${draft}_`;\n if (field.kind === 'bool') return value ? `✓ ${t.common.on}` : `✗ ${t.common.off}`;\n if (field.kind === 'dictRef') {\n if (!value) return '—';\n return truncateName(dictDisplayName || String(value), 24);\n }\n if (field.kind === 'enum') {\n const map = (t.config.enumValues as Record<string, Record<string, string> | undefined>)[\n field.labelKey\n ];\n const label = map?.[String(value)] ?? String(value);\n return `< ${label} >`;\n }\n return String(value ?? '');\n}\n\nfunction hintFor(field: FieldSpec, editing: boolean, t: Strings): string {\n if (editing) return t.config.hints.editing;\n if (field.kind === 'bool') return t.config.hints.bool;\n if (field.kind === 'enum') return t.config.hints.enum;\n if (field.kind === 'dictRef') return t.config.hints.dictRef;\n return t.config.hints.stringOrInt;\n}\n"],"mappings":"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","names":["useState","Box","Text","useInput","jsx","jsxs","FIELDS","getByPath","cfg","path","acc","k","ConfigEditor","nav","useNav","setCfg","useAppState","t","useStrings","defaultDictName","useDictName","selected","setSelected","useState","editing","setEditing","draft","setDraft","error","setError","field","currentValue","commit","raw","next","setByPath","err","useInput","input","key","d","i","idx","delta","labelW","f","visibleWidth","Box","Text","PALETTE","active","value","display","renderValue","label","pad","hintFor","dictDisplayName","truncateName"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{b as wt,e as Mt,h as Bt}from"./chunk-6ROGUGNX.js";import{a as ht,b as gt,c as xt,d as bt,e as yt,f as Tt}from"./chunk-BIBS2Q3E.js";import{e as pt}from"./chunk-EBAA2ZKH.js";import{b as _}from"./chunk-MFGIEKBU.js";import"./chunk-VTIB5Q36.js";import{a as V,b as Wt,c as Et}from"./chunk-G3DQB7FI.js";import{a as kt}from"./chunk-GULN5HRV.js";import{b as St,c as H}from"./chunk-UEJCQKZ2.js";import{c as Ct}from"./chunk-WRO5XX35.js";import{b as Q,d as B,f as a,g as It}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useState as O,useEffect as U,useRef as ct}from"react";import{Box as p,Text as x,useApp as Me,useInput as j}from"ink";function At(t,e=Math.random){let r=[...t];for(let n=r.length-1;n>0;n--){let o=Math.floor(e()*(n+1)),u=r[n];r[n]=r[o],r[o]=u}return r}function Pt(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 vt(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 Rt(t,e,r){if(e==="random"){let n=r===void 0?Math.random:Pt(r);return At(t,n)}return t}function G(t){return{target:t,typed:"",errorsThisWord:0}}function $t(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 Z(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:G(t[0].name)},finishedAt:null,playlist:t}}function tt(t,e,r=Date.now()){if(!t.current)return{session:t,effect:"none"};let{state:n,effect:o}=$t(t.current.input,e);if(o==="correct"){let u={word:n.target,errors:n.errorsThisWord,durationMs:r-t.current.wordStartedAt},m=t.current.wordIndex+1,c=[...t.results,u];return m>=t.playlist.length?{session:{...t,results:c,current:null,finishedAt:r},effect:o}:{session:{...t,results:c,current:{wordIndex:m,wordStartedAt:r,input:G(t.playlist[m].name)}},effect:o}}return{session:{...t,current:{...t.current,input:n}},effect:o}}function Dt(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,o=[...t.results,r];return n>=t.playlist.length?{session:{...t,results:o,current:null,finishedAt:e},effect:"skipped"}:{session:{...t,results:o,current:{wordIndex:n,wordStartedAt:e,input:G(t.playlist[n].name)}},effect:"skipped"}}function et(t){let e=t.results.reduce((o,u)=>o+u.errors,0),r=(t.finishedAt??Date.now())-t.startedAt,n={};for(let o of t.results)o.errors>0&&(n[o.word]=(n[o.word]??0)+o.errors);return{wordCount:t.results.length,errors:e,durationMs:r,perWordErrors:n}}import{useEffect as Nt,useReducer as se,useRef as ue,useState as le}from"react";import{useInput as de,useApp as me}from"ink";function fe(t,e){if(e.type==="start")return{session:Z(e.playlist,e.now),lastEffect:null,effectSeq:0};if(e.type==="skip"){let r=Dt(t.session,e.now);return{session:r.session,lastEffect:r.effect,effectSeq:t.effectSeq+1}}if(e.type==="event"){if(e.key.backspace||e.key.delete){let o=tt(t.session,{type:"backspace"},e.now);return{session:o.session,lastEffect:o.effect,effectSeq:t.effectSeq+1}}if(e.input.length===0)return t;let r=t.session,n=t.lastEffect;for(let o of e.input){let u=tt(r,{type:"char",ch:o},e.now);if(r=u.session,n=u.effect,r.finishedAt!==null)break}return{session:r,lastEffect:n,effectSeq:t.effectSeq+1}}return t}function pe(t){let e=[...t];if(e.some(n=>n.codePointAt(0)>=128))return{kind:"ime",cleaned:""};let r=e.filter(n=>{let o=n.codePointAt(0);return o>=32&&o<=126}).join("");return{kind:r.length>0?"valid":"noise",cleaned:r}}function Lt({playlist:t,onComplete:e,onTab:r,onEscape:n,onSkip:o,onImeBlock:u,onValidInput:m,enabled:c=!0}){let[l,b]=se(fe,void 0,()=>({session:Z(t,Date.now()),lastEffect:null,effectSeq:0})),C=ue(!1),[T,w]=le(0),{exit:S}=me();return de((f,h)=>{if(h.ctrl&&f==="c"){S();return}if(h.ctrl&&f==="n"){o?.(),b({type:"skip",now:Date.now()});return}if(h.escape){n?.();return}if(h.tab){r?.();return}if(h.upArrow||h.downArrow||h.leftArrow||h.rightArrow||h.return||h.ctrl||h.meta)return;let{kind:M,cleaned:A}=pe(f);if(M==="ime"){u?.();return}M!=="noise"&&(m?.(),b({type:"event",input:A,key:h,now:Date.now()}))},{isActive:c}),Nt(()=>{l.session.finishedAt!==null&&!C.current&&(C.current=!0,e(l.session))},[l.session,e]),Nt(()=>{if(l.session.finishedAt!==null)return;let f=setInterval(()=>w(h=>h+1),1e3);return()=>clearInterval(f)},[l.session.finishedAt]),{session:l.session,lastEffect:l.lastEffect,effectSeq:l.effectSeq,tick:T}}import{useEffect as he,useRef as ge}from"react";function Ht(t){let e=ge(!1);return he(()=>{e.current||(e.current=!0,ht(!t.enabled).catch(()=>{}))},[t.enabled]),{keystroke:()=>t.enabled&>(),correct:()=>t.enabled&&xt(),wrong:()=>t.enabled&&bt(),pronounce:r=>{t.enabled&&t.autoplayPronunciation&&yt(r,t.accent,t.pronunciationRate,t.pronunciationSource)},prefetch:r=>{t.enabled&&Tt(r,t.accent,t.pronunciationSource)}}}import{useCallback as xe}from"react";function Ot(t){return xe(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 kt(r),Bt({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(([,u])=>u>0);if(n.length===0)return;let o=await V();for(let[u,m]of n)o=Et(o,u,t.dictId,m);await Wt(o)},[t.dictId,t.chapterIndex,t.mode])}function be(t=process.env){return t.KITTY_WINDOW_ID?{supportsDoubleHeight:!1}:t.VTE_VERSION?{supportsDoubleHeight:!1}:t.KONSOLE_VERSION?{supportsDoubleHeight:!1}:t.WT_SESSION?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="Apple_Terminal"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="iTerm.app"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="WezTerm"?{supportsDoubleHeight:!0}:t.TERM_PROGRAM==="mintty"?{supportsDoubleHeight:!0}:t.ALACRITTY_SOCKET||t.ALACRITTY_LOG?{supportsDoubleHeight:!0}:t.XTERM_VERSION?{supportsDoubleHeight:!0}:{supportsDoubleHeight:!1}}var rt=null;function ye(){return rt||(rt=be()),rt}function F(t,e,r,n=ye()){return t==="standard"||!(t==="huge"||n.supportsDoubleHeight)?!1:r>=2*e+4}import{Box as Te,Text as nt,Transform as _t,useStdout as we}from"ink";import{jsx as ot,jsxs as it}from"react/jsx-runtime";function K({target:t,typed:e,error:r=!1,hideTarget:n=!1,align:o="center",framePadX:u=0}){let{stdout:m}=we(),c=m?.columns??80,l=[...t],b=[...e],C=Math.floor(c/2),T=o==="center"?Math.max(0,Math.floor((C-l.length)/2)):0,w=" ".repeat(Math.max(0,T-u)),S=()=>l.map((f,h)=>{let M=h<b.length,A=n&&!M?"_":M?b[h]:f,R=r?a.error:M?a.accent:a.muted;return ot(nt,{bold:!0,color:R,children:A},h)});return it(Te,{flexDirection:"column",width:o==="center"?"100%":void 0,paddingY:o==="center"?2:0,children:[ot(_t,{transform:f=>`\x1B#3${f}`,children:it(nt,{children:[w,S()]})}),ot(_t,{transform:f=>`\x1B#4${f}`,children:it(nt,{children:[w,S()]})})]})}import{jsx as jt}from"react/jsx-runtime";function qt({framePadX:t=0,...e}){let{cfg:r}=_(),n=process.stdout?.columns??80;return F(r.wordDisplay,[...e.target].length,n)?jt(K,{...e,framePadX:t}):jt(It,{...e})}import{Box as P,Text as d,useStdout as Se}from"ink";import{Fragment as Ce,jsx as s,jsxs as v}from"react/jsx-runtime";var Vt=28;function Gt(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function Ie(){let{stdout:t}=Se(),e=t?.columns??80;return Math.max(20,e-Vt)}function W({left:t,right:e}){let r=Ie();return v(P,{children:[s(P,{width:r,children:t}),s(P,{width:Vt,justifyContent:"flex-end",children:e})]})}function Ft(t){let e=B(),r=[...t.target],n=[...t.typed],o=v(P,{children:[r.map((S,f)=>{let h=f<n.length,M=t.hideTarget&&!h?"_":h?n[f]:S,A=t.error?a.error:h?a.accent:a.muted;return s(d,{bold:!0,color:A,children:M},f)}),t.phonetic&&v(Ce,{children:[s(d,{children:" "}),s(d,{italic:!0,dimColor:!0,color:a.muted,children:t.phonetic})]})]}),u=t.translation.length>0?s(d,{color:a.primary,children:t.translation[0]}):s(d,{children:" "}),m=t.phonetic?s(d,{italic:!0,dimColor:!0,color:a.muted,children:t.phonetic}):s(d,{children:" "}),c=t.info,l=Number.isInteger(c.accPct)?`${c.accPct}`:c.accPct.toFixed(1),b=!t.imeBlocked&&t.audioWarning!==null,C=t.imeBlocked?s(d,{color:a.warning,children:e.practice.imeWarningShort}):b?s(d,{color:a.warning,children:e.practice.audioWarningShort}):c.visible?s(d,{color:a.muted,children:`${c.dictName} \xB7 ${c.chapterLabel}`}):s(d,{children:" "}),T=t.imeBlocked||b?s(d,{children:" "}):c.visible?s(d,{color:a.muted,children:`${c.completed}/${c.total} \xB7 ${c.wpm}wpm \xB7 ${l}%`}):s(d,{children:" "}),w=t.imeBlocked||b?s(d,{children:" "}):c.visible?s(d,{color:a.muted,children:Gt(c.elapsedMs)}):s(d,{children:" "});return t.huge?v(P,{flexDirection:"column",children:[s(K,{target:t.target,typed:t.typed,error:t.error,hideTarget:t.hideTarget,align:"left"}),s(P,{children:s(d,{children:" "})}),s(W,{left:m,right:C}),s(W,{left:u,right:T}),s(W,{left:s(d,{children:" "}),right:w})]}):v(P,{flexDirection:"column",children:[s(W,{left:o,right:C}),s(W,{left:s(d,{children:" "}),right:T}),s(W,{left:u,right:w})]})}function Kt(){let t=B();return v(P,{flexDirection:"column",children:[s(W,{left:s(d,{color:a.warning,children:t.stealth.paused}),right:s(d,{color:a.muted,children:t.stealth.pausedHintRight})}),s(W,{left:s(d,{children:" "}),right:v(d,{color:a.muted,children:["Esc ",t.common.back]})}),s(W,{left:s(d,{children:" "}),right:s(d,{children:" "})})]})}function Xt(t){let e=B(),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 ${Gt(t.durationMs)}`;return v(P,{flexDirection:"column",children:[s(W,{left:s(d,{color:a.success,children:n}),right:s(d,{color:a.muted,children:e.stealth.nextHintRight})}),s(W,{left:s(d,{children:" "}),right:v(d,{color:a.muted,children:["Esc ",e.common.back]})}),s(W,{left:s(d,{children:" "}),right:s(d,{children:" "})})]})}import{jsx as i,jsxs as E}from"react/jsx-runtime";function Nr({params:t}){let{dictId:e,chapterIndex:r,mode:n}=t,{cfg:o}=_(),u=B(),[m,c]=O("loading"),[l,b]=O(null),[C,T]=O(null);return U(()=>{let w=!1;return c("loading"),b(null),T(null),(async()=>{try{let S=await pt(e);if(w)return;if(n==="review"){let A=await V();if(w)return;let R=S.filter(q=>A[q.name]?.count).slice(0,o.chapterSize);if(R.length===0){T(u.practice.errors.noMistakes),c("error");return}b({playlist:R,totalChapters:1}),c("typing");return}let f=vt(S,o.chapterSize);if(f.length===0){T(u.practice.errors.dictEmpty(e)),c("error");return}let h=Math.max(0,Math.min(f.length-1,r)),M=Rt(f[h],n);b({playlist:M,totalChapters:f.length}),c("typing")}catch(S){if(w)return;T(S.message),c("error")}})(),()=>{w=!0}},[e,r,n,o.chapterSize,u]),m==="loading"?i(ve,{text:u.practice.loading,color:a.muted}):m==="error"?i(Ae,{msg:C??u.practice.errors.unknown}):l?i(ke,{params:t,loaded:l,phase:m,setPhase:c},`${e}-${r}-${n}-${t.stealth?"s":"n"}`):null}function ke({params:t,loaded:e,phase:r,setPhase:n}){let{dictId:o,chapterIndex:u,mode:m}=t,c=t.stealth===!0,{cfg:l}=_(),b=Q(),{exit:C}=Me(),T=()=>b.stack.length>1?b.back():C(),w=Ot({dictId:o,chapterIndex:u,mode:m}),S=St(o),f=Ht({enabled:!c&&l.sounds.master,accent:l.accent,autoplayPronunciation:!c&&l.autoplayPronunciation,pronunciationRate:l.sounds.pronunciationRate,pronunciationSource:l.sounds.pronunciationSource}),h=wt(),M=l.sounds.master?h.warning:null,A=ct(!1),R=ct(0),q=ct(-1),[zt,st]=O(!1),[ut,Jt]=O(null),[lt,dt]=O(!1);U(()=>{if(ut===null)return;let g=setTimeout(()=>st(!1),2e3);return()=>clearTimeout(g)},[ut]);let{session:I,lastEffect:$,effectSeq:Y,tick:Qt}=Lt({playlist:e.playlist,enabled:r==="typing",onComplete:g=>{A.current||(A.current=!0,n("summary"),Promise.resolve(w(et(g))).catch(y=>{console.error("Failed to persist session:",y)}))},onEscape:()=>n(r==="paused"?"typing":"paused"),onTab:c?void 0:()=>{let g=I.current?e.playlist[I.current.wordIndex]:void 0;g&&f.pronounce(g.name)},onImeBlock:()=>dt(!0),onValidInput:()=>dt(!1)});U(()=>{c||Y!==R.current&&(R.current=Y,$!==null&&($==="wrong"&&l.sounds.feedback&&f.wrong(),$==="progress"&&l.sounds.keystroke&&f.keystroke(),$==="correct"&&(l.sounds.feedback&&f.correct(),l.sounds.keystroke&&f.keystroke())))},[c,Y,$,f,l.sounds.feedback,l.sounds.keystroke]),U(()=>{if(c)return;let g=I.current?.wordIndex??-1;if(g===-1||g===q.current)return;q.current=g;let y=e.playlist[g],N=e.playlist[g+1];y&&l.autoplayPronunciation&&f.pronounce(y.name),N&&f.prefetch(N.name)},[c,I.current?.wordIndex,f,l.autoplayPronunciation,e.playlist]),j((g,y)=>{if(y.tab){st(!0),Jt(Date.now());return}},{isActive:c&&r==="typing"}),j((g,y)=>{if(y.return){n("typing");return}if(y.escape){T();return}},{isActive:r==="paused"}),j((g,y)=>{y.ctrl&&g==="c"&&(Mt(!0),C())},{isActive:c&&r==="paused"}),j((g,y)=>{if(y.escape){T();return}if(y.return){let N=u+1;m==="loop"?b.replace({name:"practice",params:{dictId:o,chapterIndex:u,mode:m,stealth:t.stealth}}):m==="review"||N>=e.totalChapters?T():b.replace({name:"practice",params:{dictId:o,chapterIndex:N,mode:m,stealth:t.stealth}});return}if(g==="m"){b.replace({name:"practice",params:{dictId:o,chapterIndex:0,mode:"review",stealth:t.stealth}});return}},{isActive:r==="summary"});let D=I.results.length,Zt=I.results.reduce((g,y)=>g+y.errors,0),z=Date.now()-I.startedAt,mt=z/6e4,ft=mt>0?Math.round(D/mt*10)/10:0,k=r==="summary"?et(I):null;if(c){if(r==="paused")return i(Kt,{});if(r==="summary"&&k){let L=k.durationMs/6e4,oe=L>0?Math.round(k.wordCount/L*10)/10:0,ie=Object.keys(k.perWordErrors).length,ce=k.wordCount===0?1:Math.max(0,(k.wordCount-ie)/k.wordCount),ae=Math.round(ce*1e3)/10;return i(Xt,{wordCount:k.wordCount,errors:k.errors,durationMs:k.durationMs,wpm:oe,accPct:ae})}let g=I.current?e.playlist[I.current.wordIndex]:e.playlist[e.playlist.length-1],y=I.current?.input??{target:"",typed:"",errorsThisWord:0},N=new Set(I.results.filter(L=>L.errors>0).map(L=>L.word)).size,ee=D===0?1:Math.max(0,(D-N)/D),re=Math.round(ee*1e3)/10,ne=m==="review"?"review":`ch ${u+1}/${e.totalChapters}`;return i(Ft,{target:g?.name??"",typed:y.typed,hideTarget:m==="dictation",phonetic:Ut(g,l.accent),translation:g?.trans??[],error:$==="wrong",huge:F(l.wordDisplay,[...g?.name??""].length,process.stdout.columns??80),imeBlocked:lt,audioWarning:M,info:{visible:zt,dictName:H(S,24),chapterLabel:ne,completed:D,total:e.playlist.length,wpm:ft,accPct:re,elapsedMs:z}})}if(r==="paused")return i(Be,{dictName:S,chapterIndex:u,totalChapters:e.totalChapters,mode:m,completed:D,total:e.playlist.length});if(r==="summary"&&k)return i(Re,{dictName:S,chapterIndex:u,totalChapters:e.totalChapters,mode:m,summary:k});let J=I.current?e.playlist[I.current.wordIndex]:e.playlist[e.playlist.length-1],te=I.current?.input??{target:"",typed:"",errorsThisWord:0};return i(We,{dictName:S,chapterIndex:u,totalChapters:e.totalChapters,mode:m,accent:l.accent,completed:D,total:e.playlist.length,errors:Zt,wpm:ft,elapsedMs:z,target:J?.name??"",typed:te.typed,flashError:$==="wrong",hideTarget:m==="dictation",phonetic:Ut(J,l.accent),translation:J?.trans??[],imeBlocked:lt,audioWarning:M})}function Ut(t,e){if(!t)return null;let r=e==="us"?t.usphone:t.ukphone;return r?Ct(r):null}function at(t){let e=Math.floor(t/1e3),r=Math.floor(e/60),n=e%60;return`${r}:${String(n).padStart(2,"0")}`}function We(t){let e=B(),r=t.total===0?0:t.completed/t.total;return E(p,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[i(Ee,{dictName:t.dictName,chapterIndex:t.chapterIndex,totalChapters:t.totalChapters,mode:t.mode,accent:t.accent,completed:t.completed,total:t.total,elapsedMs:t.elapsedMs}),E(p,{flexGrow:1,flexDirection:"column",alignItems:"center",justifyContent:"center",children:[i(qt,{target:t.target,typed:t.typed,error:t.flashError,hideTarget:t.hideTarget,framePadX:2}),t.phonetic&&i(p,{marginTop:1,children:i(x,{italic:!0,dimColor:!0,color:a.muted,children:t.phonetic})}),t.translation.length>0&&i(p,{marginTop:1,flexDirection:"column",alignItems:"center",children:t.translation.slice(0,2).map((n,o)=>i(x,{color:a.primary,children:n},o))}),t.imeBlocked&&i(p,{marginTop:1,children:i(x,{color:a.warning,children:e.practice.imeWarning})}),!t.imeBlocked&&t.audioWarning&&i(p,{marginTop:1,children:i(x,{color:a.warning,children:t.audioWarning})})]}),E(p,{flexDirection:"column",children:[i(Yt,{frac:r}),i(p,{justifyContent:"center",marginTop:1,children:E(x,{color:a.muted,children:[t.completed,"/",t.total," \xB7 ",at(t.elapsedMs)," \xB7 ",t.wpm," ",e.practice.statCards.wpm," \xB7 ",t.errors," ",e.practice.statCards.errors]})}),i(p,{justifyContent:"center",marginTop:1,children:i(x,{color:a.muted,children:e.practice.footers.typing})})]})]})}function Ee(t){let e=B(),r=e.practice.modes[t.mode],n=e.practice.accents[t.accent],o=H(t.dictName,20),u=t.mode==="review"?`${o} \xB7 ${e.practice.reviewLabel} \xB7 ${n}`:`${o} \xB7 ${e.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${r} \xB7 ${n}`,m=`${t.completed}/${t.total} \xB7 ${at(t.elapsedMs)}`;return E(p,{children:[i(x,{color:a.muted,children:u}),i(p,{flexGrow:1}),i(x,{color:a.muted,children:m})]})}function Yt({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))),o=r-n;return E(p,{justifyContent:"center",children:[i(x,{color:a.accent,children:"\u2501".repeat(n)}),i(x,{color:a.muted,children:"\u2500".repeat(o)})]})}function Be(t){let e=B(),r=t.total===0?0:t.completed/t.total,n=t.mode==="review"?`${H(t.dictName,20)} \xB7 ${e.practice.reviewLabel}`:`${H(t.dictName,20)} \xB7 ${e.practice.pause.chapter(t.chapterIndex+1,t.totalChapters)}`;return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{bold:!0,color:a.warning,children:e.practice.pause.title}),i(p,{marginTop:1,children:i(x,{color:a.muted,children:n})}),i(p,{marginTop:2,children:i(Yt,{frac:r})}),i(p,{marginTop:1,children:i(x,{color:a.muted,children:e.practice.pause.progress(t.completed,t.total)})}),i(p,{marginTop:2,children:i(x,{color:a.muted,children:e.practice.pause.hint})})]})}function Ae({msg:t}){let e=B();return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[i(x,{color:a.error,children:t}),i(p,{marginTop:2,children:E(x,{color:a.muted,children:["Esc ",e.common.back]})}),i(Pe,{})]})}function Pe(){let t=Q();return j((e,r)=>{r.escape&&t.back()}),null}function ve({text:t,color:e}){return i(p,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(x,{color:e,children:t})})}function Re(t){let{summary:e}=t,r=e.durationMs/6e4,n=r>0?Math.round(e.wordCount/r*10)/10:0,o=Object.keys(e.perWordErrors).length,u=e.wordCount===0?1:Math.max(0,(e.wordCount-o)/e.wordCount),m=Math.round(u*1e3)/10,c=B(),l=c.practice.modes[t.mode],b=H(t.dictName,20),C=t.mode==="review"?`${b} \xB7 ${c.practice.reviewLabel}`:`${b} \xB7 ${c.practice.chapterLabel(t.chapterIndex+1,t.totalChapters)} \xB7 ${l}`,w=`Enter ${t.mode==="loop"?c.practice.summary.loopAgain:t.mode==="review"||t.chapterIndex+1>=t.totalChapters?c.practice.summary.backMenu:c.practice.summary.nextChapter} \xB7 m ${c.practice.summary.reviewMistakes} \xB7 Esc ${c.practice.summary.backMenu}`;return E(p,{flexDirection:"column",alignItems:"center",justifyContent:"center",paddingY:1,width:"100%",height:"100%",children:[i(x,{bold:!0,color:a.success,children:c.practice.chapterComplete}),i(p,{marginTop:1,children:i(x,{color:a.muted,children:C})}),E(p,{marginTop:3,flexDirection:"row",justifyContent:"center",children:[i(X,{label:c.practice.statCards.words,value:String(e.wordCount),color:a.text}),i(X,{label:c.practice.statCards.errors,value:String(e.errors),color:e.errors>0?a.error:a.muted}),i(X,{label:c.practice.statCards.wpm,value:String(n),color:a.accent}),i(X,{label:c.practice.statCards.accuracy,value:`${m}%`,color:a.accent})]}),i(p,{marginTop:2,children:i(x,{color:a.muted,children:c.practice.statCards.elapsed(at(e.durationMs))})}),i(p,{flexGrow:1}),i(p,{marginTop:2,children:i(x,{color:a.muted,children:w})})]})}function X({label:t,value:e,color:r}){return E(p,{flexDirection:"column",alignItems:"center",marginX:3,children:[i(x,{bold:!0,color:r,children:e}),i(x,{color:a.muted,children:t})]})}export{Nr as PracticeScreen};
|
|
2
|
+
//# sourceMappingURL=PracticeScreen-BRPEUQJ6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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/util/term-caps.ts","../src/ui/components/BigWordHuge.tsx","../src/ui/components/BigWordAuto.tsx","../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 { useAudioStatus } from '../audio-context.js';\nimport { useStrings } from '../../i18n/context.js';\nimport { useDictName } from '../registry-context.js';\nimport { PALETTE } from '../components/BigWord.js';\nimport { BigWordAuto } from '../components/BigWordAuto.js';\nimport { shouldUseHuge } from '../../util/term-caps.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { wrapPhonetic } from '../../util/text.js';\nimport { StealthTyping, StealthPaused, StealthSummary } from './StealthPracticeLayout.js';\nimport { setSilentExit } from '../../util/post-exit-action.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 pronunciationRate: cfg.sounds.pronunciationRate,\n pronunciationSource: cfg.sounds.pronunciationSource,\n });\n const audioStatus = useAudioStatus();\n // Only surface the warning when the user opted in to sounds — if they\n // disabled sounds via config, \"no player found\" isn't a problem to flag.\n const audioWarn = cfg.sounds.master ? audioStatus.warning : null;\n\n const finishedRef = useRef(false);\n const lastEffectSeqRef = useRef<number>(0);\n const lastIndexRef = useRef<number>(-1);\n const [infoVisible, setInfoVisible] = useState(false);\n const [infoShownAt, setInfoShownAt] = useState<number | null>(null);\n const [imeBlocked, setImeBlocked] = useState(false);\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, effectSeq, 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 onImeBlock: () => setImeBlocked(true),\n onValidInput: () => setImeBlocked(false),\n });\n\n useEffect(() => {\n if (stealth) return;\n if (effectSeq === lastEffectSeqRef.current) return;\n lastEffectSeqRef.current = effectSeq;\n if (lastEffect === null) return;\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, effectSeq, 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 // Stealth + paused only: Ctrl+C exits silently — erase the 3 inline rows\n // (handled in practice.impl.ts after waitUntilExit) and skip the session\n // report. Normal Ctrl+C from typing keeps the 3 rows in scrollback.\n useInput(\n (input, key) => {\n if (key.ctrl && input === 'c') {\n setSilentExit(true);\n exit();\n }\n },\n { isActive: stealth && 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 huge={shouldUseHuge(\n cfg.wordDisplay,\n [...(currentWord?.name ?? '')].length,\n process.stdout.columns ?? 80,\n )}\n imeBlocked={imeBlocked}\n audioWarning={audioWarn}\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 imeBlocked={imeBlocked}\n audioWarning={audioWarn}\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 ? wrapPhonetic(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 imeBlocked: boolean;\n audioWarning: string | null;\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 <BigWordAuto\n target={props.target}\n typed={props.typed}\n error={props.flashError}\n hideTarget={props.hideTarget}\n framePadX={2}\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\n {props.imeBlocked && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{t.practice.imeWarning}</Text>\n </Box>\n )}\n\n {!props.imeBlocked && props.audioWarning && (\n <Box marginTop={1}>\n <Text color={PALETTE.warning}>{props.audioWarning}</Text>\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\n// effectSeq is a monotonic counter bumped on every dispatch that resolves into\n// a new lastEffect value. Consumers (PracticeScreen audio effect) depend on it\n// instead of lastEffect alone because React's Object.is comparison treats two\n// consecutive 'progress' strings as equal — without a counter, useEffect would\n// fire on only the first char of a word, dropping the keystroke sound on every\n// subsequent character.\ntype LoopState = { session: Session; lastEffect: InputEffect | null; effectSeq: number };\n\nfunction reducer(state: LoopState, action: Action): LoopState {\n if (action.type === 'start') {\n return { session: startSession(action.playlist, action.now), lastEffect: null, effectSeq: 0 };\n }\n if (action.type === 'skip') {\n const r = skipSession(state.session, action.now);\n return { session: r.session, lastEffect: r.effect, effectSeq: state.effectSeq + 1 };\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, effectSeq: state.effectSeq + 1 };\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, effectSeq: state.effectSeq + 1 };\n }\n return state;\n}\n\n// Classify a typed batch from Ink's useInput. Single source of truth so the\n// IME-detection logic stays unit-testable without rendering Ink.\n// kind='ime' → contains at least one non-ASCII codepoint (>= 0x80, e.g.\n// CJK / Latin-1 supplement); reject the batch and show hint.\n// kind='valid' → all-ASCII printable; `cleaned` has the dispatchable subset.\n// kind='noise' → ASCII control (DEL, NUL, etc.) or empty; ignore silently.\nexport type InputBatchKind = 'ime' | 'valid' | 'noise';\nexport function classifyInputBatch(input: string): { kind: InputBatchKind; cleaned: string } {\n const chars = [...input];\n if (chars.some((c) => c.codePointAt(0)! >= 0x80)) {\n return { kind: 'ime', cleaned: '' };\n }\n const cleaned = chars\n .filter((c) => {\n const cp = c.codePointAt(0)!;\n return cp >= 0x20 && cp <= 0x7e;\n })\n .join('');\n return { kind: cleaned.length > 0 ? 'valid' : 'noise', cleaned };\n}\n\nexport type UseWordLoopOpts = {\n playlist: Word[];\n onComplete: (session: Session) => void;\n onTab?: () => void;\n onEscape?: () => void;\n onSkip?: () => void;\n // Fires when the typed batch contains any codepoint > 0x7E — almost always\n // means the user's IME is active (CJK input method). The batch is rejected\n // entirely so the wrong characters aren't fed to the session.\n onImeBlock?: () => void;\n // Fires when a non-empty all-ASCII batch is about to dispatch. Used to\n // clear a previously-set IME warning so it auto-dismisses on the first\n // successful keystroke.\n onValidInput?: () => void;\n enabled?: boolean;\n};\n\nexport function useWordLoop({\n playlist,\n onComplete,\n onTab,\n onEscape,\n onSkip,\n onImeBlock,\n onValidInput,\n enabled = true,\n}: UseWordLoopOpts) {\n const [state, dispatch] = useReducer(reducer, undefined, () => ({\n session: startSession(playlist, Date.now()),\n lastEffect: null as InputEffect | null,\n effectSeq: 0,\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 { kind, cleaned } = classifyInputBatch(input);\n if (kind === 'ime') {\n onImeBlock?.();\n return;\n }\n if (kind === 'noise') return;\n onValidInput?.();\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, effectSeq: state.effectSeq, tick };\n}\n\n// Exposed for unit tests only — verifies effectSeq monotonic increment and\n// effect dispatch behavior without mounting a full Ink tree.\nexport const __test = { reducer };\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 pronunciationRate: number;\n pronunciationSource: 'youdao' | 'dictapi';\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)\n void playPronunciation(word, opts.accent, opts.pronunciationRate, opts.pronunciationSource);\n },\n prefetch: (word) => {\n if (!opts.enabled) return;\n void prefetchPronunciation(word, opts.accent, opts.pronunciationSource);\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","// Detect terminal capabilities relevant to BigWord rendering.\n//\n// VT100 DECDWL/DECDHL (ESC#3 / ESC#4 / ESC#6) let us render the practice\n// word using the terminal's real font at 2× width and 2× height. Support\n// varies: macOS Terminal.app, Windows Terminal, iTerm2, WezTerm, xterm,\n// mintty, Alacritty render correctly; kitty / VTE-based / Konsole either\n// skip the attribute or render glitchy output.\n//\n// Detection is env-only — runtime device-attribute queries (ESC[c) would\n// race against Ink's input pipeline and aren't worth the complexity.\n\nexport type TermCaps = {\n supportsDoubleHeight: boolean;\n};\n\nexport function detectTermCaps(env: NodeJS.ProcessEnv = process.env): TermCaps {\n if (env.KITTY_WINDOW_ID) return { supportsDoubleHeight: false };\n if (env.VTE_VERSION) return { supportsDoubleHeight: false };\n if (env.KONSOLE_VERSION) return { supportsDoubleHeight: false };\n\n if (env.WT_SESSION) return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'Apple_Terminal') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'iTerm.app') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'WezTerm') return { supportsDoubleHeight: true };\n if (env.TERM_PROGRAM === 'mintty') return { supportsDoubleHeight: true };\n if (env.ALACRITTY_SOCKET || env.ALACRITTY_LOG) return { supportsDoubleHeight: true };\n if (env.XTERM_VERSION) return { supportsDoubleHeight: true };\n\n return { supportsDoubleHeight: false };\n}\n\nlet cachedCaps: TermCaps | null = null;\nexport function getTermCaps(): TermCaps {\n if (!cachedCaps) cachedCaps = detectTermCaps();\n return cachedCaps;\n}\n\nexport type WordDisplay = 'auto' | 'huge' | 'standard';\n\n// Decide whether the practice word should render via DECDHL (2×2 huge).\n// `huge` forces it (subject to fitting the width); `auto` defers to terminal\n// detection; `standard` never. The fit guard keeps a long word from wrapping\n// at 2× width on a narrow terminal — fall back to single-cell instead.\nexport function shouldUseHuge(\n wordDisplay: WordDisplay,\n targetLength: number,\n cols: number,\n caps: TermCaps = getTermCaps(),\n): boolean {\n if (wordDisplay === 'standard') return false;\n const supported = wordDisplay === 'huge' || caps.supportsDoubleHeight;\n if (!supported) return false;\n return cols >= 2 * targetLength + 4;\n}\n","import { Box, Text, Transform, useStdout } from 'ink';\nimport { PALETTE } from './BigWord.js';\n\n// Renders the practice word using VT100 DECDHL (double-height + double-width\n// line attribute). The terminal draws the SAME glyphs as a single-cell line\n// but at 2× scale — real terminal font, no rasterization, no figlet/ASCII art.\n//\n// DECDHL needs two physical rows with identical content: ESC#3 (top half) and\n// ESC#4 (bottom half); the terminal merges them into one visual 2×-tall row.\n//\n// Why <Transform> and not a raw-escape string inside <Text>: Ink's text\n// pipeline (slice-ansi / wrap-ansi) only understands SGR escapes. A bare\n// `\\x1b#3` desyncs its parser and leaks fragments of the following color\n// codes as visible characters (e.g. \"magma\" → \"m8ma8mm8ma\"). <Transform>\n// runs AFTER layout/slicing, so it prepends the line attribute to the\n// already-composed line without corrupting it, and Ink still handles the\n// per-glyph SGR coloring natively.\n//\n// Centering: DECDHL doubles every cell, so the usable width is cols/2 logical\n// columns, not cols. Center the word within cols/2, then subtract framePadX —\n// the ancestor's left padding (e.g. TypingLayout's paddingX) sits on the same\n// physical line and gets doubled too, so it counts toward the left margin. We\n// also force width=\"100%\" so a centering parent (alignItems=\"center\") doesn't\n// insert its OWN single-width pad before the line attribute (that pad would\n// double and shove the word off the right edge).\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n align?: 'center' | 'left';\n framePadX?: number;\n};\n\nexport function BigWordHuge({\n target,\n typed,\n error = false,\n hideTarget = false,\n align = 'center',\n framePadX = 0,\n}: Props) {\n const { stdout } = useStdout();\n const cols = stdout?.columns ?? 80;\n const chars = [...target];\n const typedChars = [...typed];\n\n const visibleCols = Math.floor(cols / 2);\n const totalLead =\n align === 'center' ? Math.max(0, Math.floor((visibleCols - chars.length) / 2)) : 0;\n const pad = ' '.repeat(Math.max(0, totalLead - framePadX));\n\n const glyphs = () =>\n chars.map((ch, i) => {\n const isTyped = i < typedChars.length;\n const display = hideTarget && !isTyped ? '_' : isTyped ? typedChars[i]! : ch;\n const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;\n return (\n <Text key={i} bold color={color}>\n {display}\n </Text>\n );\n });\n\n return (\n <Box\n flexDirection=\"column\"\n width={align === 'center' ? '100%' : undefined}\n paddingY={align === 'center' ? 2 : 0}\n >\n <Transform transform={(line) => `\\x1b#3${line}`}>\n <Text>\n {pad}\n {glyphs()}\n </Text>\n </Transform>\n <Transform transform={(line) => `\\x1b#4${line}`}>\n <Text>\n {pad}\n {glyphs()}\n </Text>\n </Transform>\n </Box>\n );\n}\n","import { useAppState } from '../app-state.js';\nimport { shouldUseHuge } from '../../util/term-caps.js';\nimport { BigWord } from './BigWord.js';\nimport { BigWordHuge } from './BigWordHuge.js';\n\ntype Props = {\n target: string;\n typed: string;\n error?: boolean;\n hideTarget?: boolean;\n // Horizontal padding the surrounding layout reserves on the left. The huge\n // (DECDHL) renderer needs it to center correctly; the standard renderer\n // ignores it.\n framePadX?: number;\n};\n\nexport function BigWordAuto({ framePadX = 0, ...props }: Props) {\n const { cfg } = useAppState();\n const cols = process.stdout?.columns ?? 80;\n const huge = shouldUseHuge(cfg.wordDisplay, [...props.target].length, cols);\n return huge ? <BigWordHuge {...props} framePadX={framePadX} /> : <BigWord {...props} />;\n}\n","import type { ReactNode } from 'react';\nimport { Box, Text, useStdout } from 'ink';\nimport { PALETTE } from '../components/BigWord.js';\nimport { BigWordHuge } from '../components/BigWordHuge.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 huge: boolean;\n imeBlocked: boolean;\n audioWarning: string | null;\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 t = useStrings();\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 // In huge mode the word lives on its own two DECDHL lines, so the phonetic\n // (normally inline after the word) gets its own row below.\n const phoneticCell = props.phonetic ? (\n <Text italic dimColor color={PALETTE.muted}>\n {props.phonetic}\n </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 priority: imeBlocked > audioWarning > info > idle.\n // imeBlocked: warning indicator on row 1, rows 2/3 blank.\n // audioWarning (and no IME issue): `! audio` short marker on row 1, rows 2/3 blank.\n // info.visible: dict / progress / time on rows 1/2/3.\n // idle: all three rows blank.\n const showAudio = !props.imeBlocked && props.audioWarning !== null;\n const right1 = props.imeBlocked ? (\n <Text color={PALETTE.warning}>{t.practice.imeWarningShort}</Text>\n ) : showAudio ? (\n <Text color={PALETTE.warning}>{t.practice.audioWarningShort}</Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.dictName} · ${info.chapterLabel}`}</Text>\n ) : (\n <Text> </Text>\n );\n const right2 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{`${info.completed}/${info.total} · ${info.wpm}wpm · ${accFmt}%`}</Text>\n ) : (\n <Text> </Text>\n );\n const right3 = props.imeBlocked || showAudio ? (\n <Text> </Text>\n ) : info.visible ? (\n <Text color={PALETTE.muted}>{fmtTime(info.elapsedMs)}</Text>\n ) : (\n <Text> </Text>\n );\n\n // Huge layout: the word occupies its own two DECDHL lines (full physical\n // lines — nothing on the right, or the line attribute would double the info\n // chips too). Phonetic / translation / time move to the three rows below,\n // preserving every info slot.\n if (props.huge) {\n return (\n <Box flexDirection=\"column\">\n <BigWordHuge\n target={props.target}\n typed={props.typed}\n error={props.error}\n hideTarget={props.hideTarget}\n align=\"left\"\n />\n {/* One blank row under the 2-row word ≈ half its height — the closest a\n character grid gets to the requested \"half-line\" gap. */}\n <Box>\n <Text> </Text>\n </Box>\n <Row left={phoneticCell} right={right1} />\n <Row left={translationCell} right={right2} />\n <Row left={<Text> </Text>} right={right3} />\n </Box>\n );\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","c","classifyInputBatch","input","chars","cleaned","cp","useWordLoop","playlist","onComplete","onTab","onEscape","onSkip","onImeBlock","onValidInput","enabled","dispatch","useReducer","completedRef","useRef","tick","setTick","useState","exit","useApp","useInput","key","kind","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","detectTermCaps","env","cachedCaps","getTermCaps","shouldUseHuge","wordDisplay","targetLength","cols","caps","Box","Text","Transform","useStdout","jsx","jsxs","BigWordHuge","target","typed","error","hideTarget","align","framePadX","stdout","useStdout","cols","chars","typedChars","visibleCols","totalLead","pad","glyphs","ch","i","isTyped","display","color","PALETTE","Text","Box","Transform","line","jsx","BigWordAuto","framePadX","props","cfg","useAppState","cols","shouldUseHuge","BigWordHuge","BigWord","Box","Text","useStdout","Fragment","jsx","jsxs","RIGHT_WIDTH","fmtTime","ms","total","m","s","useLeftWidth","stdout","useStdout","cols","Row","left","right","leftWidth","Box","StealthTyping","props","t","useStrings","target","typed","wordCell","ch","i","isTyped","display","color","PALETTE","Text","translationCell","phoneticCell","info","accFmt","showAudio","right1","right2","right3","BigWordHuge","StealthPaused","StealthSummary","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","audioStatus","useAudioStatus","audioWarn","finishedRef","useRef","lastEffectSeqRef","lastIndexRef","infoVisible","setInfoVisible","infoShownAt","setInfoShownAt","imeBlocked","setImeBlocked","id","session","lastEffect","effectSeq","tick","useWordLoop","s","sessionSummary","cur","next","useInput","_input","key","input","setSilentExit","nextIdx","completed","errors","a","r","elapsedMs","minutes","wpm","summary","StealthPaused","sMinutes","sWpm","sErrWords","sAcc","sAccPct","StealthSummary","currentWord","inputState","errWords","accFrac","accPct","chapterLabel","StealthTyping","pickPhonetic","shouldUseHuge","truncateName","PausedView","SummaryView","TypingLayout","word","accent","p","wrapPhonetic","fmtTime","ms","total","m","props","progressFrac","Box","StatusBar","BigWordAuto","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"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as j,d as F}from"./chunk-G3DQB7FI.js";import{b as N,c as $,d as O,f as V,h as Y}from"./chunk-GULN5HRV.js";import{b as B,c as y}from"./chunk-UEJCQKZ2.js";import{b as k}from"./chunk-WRO5XX35.js";import{b as R,d as C,f as o}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import"./chunk-E6BBQALJ.js";import{useEffect as G,useState as D}from"react";import{Box as i,Text as r,useInput as U,useStdout as q}from"ink";import{jsx as e,jsxs as c}from"react/jsx-runtime";var W=[7,14,30,90];function it(){let s=R(),t=C(),[n,u]=D(null),[x,g]=D(null),[d,b]=D(1);if(G(()=>{(async()=>{let[l,a]=await Promise.all([N(),j()]);u(l),g(a)})()},[]),U((l,a)=>{if(a.escape){s.back();return}a.rightArrow&&b(M=>(M+1)%W.length),a.leftArrow&&b(M=>(M-1+W.length)%W.length)}),!n||!x)return e(i,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:e(r,{color:o.muted,children:t.stats.loading})});if(n.length===0)return c(i,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[e(r,{color:o.muted,children:t.stats.none}),e(i,{marginTop:1,children:e(r,{color:o.muted,children:t.stats.nonePractice})}),e(i,{marginTop:2,children:c(r,{color:o.muted,children:["Esc ",t.common.back]})})]});let m=W[d],S=V(n),f=n.reduce((l,a)=>l+a.wordCount,0),w=n.reduce((l,a)=>l+a.errors,0),T=n.reduce((l,a)=>l+a.durationMs,0),v=n.reduce((l,a)=>l+(a.wordCount-Object.keys(a.perWordErrors).length),0),H=T>0?Math.round(f/(T/6e4)*10)/10:0,X=f===0?1:v/f,_=n.slice(-5).reverse(),I=F(x,8),p=Y(n,m),A=p.peakWpmLifetime>0?Math.min(1,p.avgWpm/p.peakWpmLifetime):0,L=Math.min(1,p.avgAccPct/100),P=m>0?Math.min(1,p.sessionsInWindow/m):0;return c(i,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[e(r,{bold:!0,color:o.accent,children:t.stats.title}),c(i,{marginTop:1,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.lifetime}),c(i,{marginTop:1,children:[e(h,{label:t.stats.sessions,value:String(n.length)}),e(h,{label:t.stats.words,value:String(f)}),e(h,{label:t.stats.errors,value:String(w)}),e(h,{label:t.stats.wpm,value:String(H),accent:!0}),e(h,{label:t.stats.accuracy,value:`${Math.round(X*1e3)/10}%`,accent:!0}),e(h,{label:t.stats.streak,value:`${S}d`,accent:!0})]})]}),c(i,{marginTop:2,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.last(m)}),c(i,{marginTop:1,flexDirection:"column",borderStyle:"round",borderColor:o.muted,paddingX:2,paddingY:0,children:[e(E,{label:t.stats.bars.speed,frac:A,valueText:`${p.avgWpm} wpm`,pct:Math.round(A*100)}),e(E,{label:t.stats.bars.accuracy,frac:L,valueText:`${p.avgAccPct}%`,pct:Math.round(L*100)}),e(E,{label:t.stats.bars.sessions,frac:P,valueText:`${p.sessionsInWindow} / ${m}`,pct:Math.round(P*100)})]})]}),c(i,{marginTop:2,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.recent}),_.map((l,a)=>e(z,{session:l,units:t.stats.recentUnits},a))]}),I.length>0&&c(i,{marginTop:2,flexDirection:"column",children:[e(r,{color:o.muted,children:t.stats.topMistakes}),I.map(([l,a])=>e(J,{word:l,count:a.count,dictIds:a.dictIds,multiSuffix:t.stats.multiDictSuffix},l))]}),e(i,{flexGrow:1}),e(i,{marginTop:1,children:e(r,{color:o.muted,children:t.stats.footer})})]})}function z({session:s,units:t}){let n=B(s.dictId),u=y(n,14);return c(i,{children:[c(r,{color:o.muted,children:[" ",s.ts.replace("T"," ").slice(0,16)," "]}),e(r,{color:o.text,children:u.padEnd(14)}),c(r,{color:o.muted,children:[" ","ch",String(s.chapter+1).padStart(3)," ",s.mode.padEnd(9)," ",String(s.wordCount).padStart(3),t.words," ",s.errors,t.errors," ",$(s),t.wpm," ",Math.round(O(s)*1e3)/10,"%"]})]})}function J({word:s,count:t,dictIds:n,multiSuffix:u}){let x=n[0]??"",g=B(x),d=n.length>1?u(n.length-1):"";return c(i,{children:[c(r,{color:o.error,children:[" ",String(t).padStart(3)," "]}),e(r,{color:o.text,children:s.padEnd(20)}),c(r,{color:o.muted,children:[y(g,20),d]})]})}function h({label:s,value:t,accent:n=!1}){return c(i,{marginRight:3,children:[c(r,{color:o.muted,children:[s," "]}),e(r,{bold:!0,color:n?o.accent:o.text,children:t})]})}function K(s){return{barWidth:Math.max(0,Math.min(60,s-11-24)),labelWidth:9,valueWidth:8,pctWidth:5,marginLeft:2}}function E({label:s,frac:t,valueText:n,pct:u}){let{stdout:x}=q(),g=x?.columns??80,d=K(g),b=Math.max(0,Math.min(1,t)),m=Math.round(d.barWidth*b),S=d.barWidth-m,f=k(s,d.labelWidth),w=k(n,d.valueWidth),T=String(u).padStart(d.pctWidth-1," ")+"%",v=" ".repeat(d.marginLeft);return c(i,{children:[e(r,{color:o.muted,children:f}),e(r,{color:o.accent,children:"\u2501".repeat(m)}),e(r,{color:o.muted,children:"\u2500".repeat(S)}),c(r,{color:o.text,children:[v,w]}),e(r,{color:o.muted,children:T})]})}export{it as StatsViewer,K as calcMetricBarLayout};
|
|
2
|
+
//# sourceMappingURL=StatsViewer-43ATH3OG.js.map
|
|
@@ -1 +1 @@
|
|
|
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';\nimport { pad } from '../../util/text.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 = outer paddingX(4) + inner border(2) + inner paddingX(4) + 1 col slack = 11.\n// The +1 slack guards against off-by-one in Ink/terminal cell rounding —\n// content was previously sized exactly to the inner width and any rounding\n// would wrap the bar onto a second line.\n// Fixed widths sum 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 = 11;\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\n// MetricBar uses pure inline <Text> siblings with CJK-aware pad() padding\n// rather than nested <Box width=N> wrappers. Mixed Box+Text flex children\n// can wrap unpredictably at terminal-width boundaries; inline text widths\n// are deterministic.\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 const labelStr = pad(label, layout.labelWidth);\n const valueStr = pad(valueText, layout.valueWidth);\n const pctStr = String(pct).padStart(layout.pctWidth - 1, ' ') + '%';\n const margin = ' '.repeat(layout.marginLeft);\n return (\n <Box>\n <Text color={PALETTE.muted}>{labelStr}</Text>\n <Text color={PALETTE.accent}>{'━'.repeat(filled)}</Text>\n <Text color={PALETTE.muted}>{'─'.repeat(empty)}</Text>\n <Text color={PALETTE.text}>{margin}{valueStr}</Text>\n <Text color={PALETTE.muted}>{pctStr}</Text>\n </Box>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","Box","Text","useInput","useStdout","jsx","jsxs","DAY_WINDOWS","StatsViewer","nav","useNav","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","labelStr","pad","valueStr","pctStr","margin"]}
|
|
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';\nimport { pad } from '../../util/text.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 = outer paddingX(4) + inner border(2) + inner paddingX(4) + 1 col slack = 11.\n// The +1 slack guards against off-by-one in Ink/terminal cell rounding —\n// content was previously sized exactly to the inner width and any rounding\n// would wrap the bar onto a second line.\n// Fixed widths sum 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 = 11;\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\n// MetricBar uses pure inline <Text> siblings with CJK-aware pad() padding\n// rather than nested <Box width=N> wrappers. Mixed Box+Text flex children\n// can wrap unpredictably at terminal-width boundaries; inline text widths\n// are deterministic.\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 const labelStr = pad(label, layout.labelWidth);\n const valueStr = pad(valueText, layout.valueWidth);\n const pctStr = String(pct).padStart(layout.pctWidth - 1, ' ') + '%';\n const margin = ' '.repeat(layout.marginLeft);\n return (\n <Box>\n <Text color={PALETTE.muted}>{labelStr}</Text>\n <Text color={PALETTE.accent}>{'━'.repeat(filled)}</Text>\n <Text color={PALETTE.muted}>{'─'.repeat(empty)}</Text>\n <Text color={PALETTE.text}>{margin}{valueStr}</Text>\n <Text color={PALETTE.muted}>{pctStr}</Text>\n </Box>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","Box","Text","useInput","useStdout","jsx","jsxs","DAY_WINDOWS","StatsViewer","nav","useNav","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","labelStr","pad","valueStr","pctStr","margin"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{c as L}from"./chunk-EBAA2ZKH.js";import"./chunk-VTIB5Q36.js";import{a as W}from"./chunk-G3DQB7FI.js";import{b as h,c as k}from"./chunk-UEJCQKZ2.js";import{c as B}from"./chunk-WRO5XX35.js";import{b as M,d as g,f as t}from"./chunk-VIOZNKSK.js";import"./chunk-NA5UNUVL.js";import{a as b}from"./chunk-E6BBQALJ.js";import{useEffect as S,useState as u}from"react";import{Box as n,Text as r,useInput as $}from"ink";import{readdir as N}from"fs/promises";import{Fragment as G,jsx as o,jsxs as d}from"react/jsx-runtime";async function P(){try{return(await N(b.dictsDir)).filter(e=>e.endsWith(".json")&&!e.endsWith(".meta.json")).map(e=>e.replace(/\.json$/,""))}catch{return[]}}function Z(){let i=M(),e=g(),[s,x]=u(""),[l,w]=u([]),[C,H]=u({}),[j,A]=u(!0),[D,f]=u(0);S(()=>{(async()=>{let a=await P(),c=[];for(let m of a){let I=await L(m);if(I)for(let E of I)c.push({dictId:m,word:E})}w(c),H(await W()),A(!1)})()},[]);let T=s.toLowerCase().trim(),p=T?l.filter(a=>a.word.name.toLowerCase().includes(T)).slice(0,50):[];if($((a,c)=>{if(c.escape){i.back();return}if(c.upArrow){f(m=>Math.max(0,m-1));return}if(c.downArrow){f(m=>Math.min(p.length-1,m+1));return}if(c.backspace||c.delete){x(m=>m.slice(0,-1)),f(0);return}a&&!c.ctrl&&!c.meta&&a.trim().length>0&&(x(m=>m+a),f(0))}),j)return o(n,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:o(r,{color:t.muted,children:e.word.indexing})});if(l.length===0)return d(n,{flexDirection:"column",alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:[o(r,{color:t.muted,children:e.word.none}),o(n,{marginTop:1,children:o(r,{color:t.muted,children:e.word.pullFirst})}),o(n,{marginTop:2,children:d(r,{color:t.muted,children:["[Esc] ",e.common.back]})})]});let y=p[D];return d(n,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",height:"100%",children:[d(n,{children:[o(r,{bold:!0,color:t.accent,children:e.word.title}),o(n,{flexGrow:1}),o(r,{color:t.muted,children:e.word.countAcross(l.length)})]}),d(n,{marginTop:1,children:[o(r,{color:t.muted,children:"> "}),o(r,{color:t.text,children:s}),o(r,{color:t.accent,children:"_"})]}),d(n,{marginTop:1,flexGrow:1,children:[d(n,{flexDirection:"column",width:"40%",children:[p.map((a,c)=>o(q,{hit:a,active:c===D},`${a.dictId}-${a.word.name}-${c}`)),p.length===0&&T&&o(r,{color:t.muted,children:e.word.noMatches(s)})]}),o(n,{flexDirection:"column",width:"60%",paddingLeft:2,children:y&&o(v,{hit:y,book:C})})]}),o(n,{marginTop:1,children:o(r,{color:t.muted,children:e.word.footer})})]})}function q({hit:i,active:e}){let s=h(i.dictId);return d(n,{children:[o(r,{color:e?t.accent:t.muted,children:e?"\u258C ":" "}),o(r,{bold:e,color:e?t.text:t.muted,children:i.word.name.padEnd(20)}),o(r,{color:t.muted,children:k(s,18)})]})}function v({hit:i,book:e}){let s=g(),x=h(i.dictId);return d(G,{children:[o(r,{bold:!0,color:t.text,children:i.word.name}),d(n,{marginTop:1,children:[i.word.usphone&&d(r,{dimColor:!0,color:t.muted,children:["US ",B(i.word.usphone)," "]}),i.word.ukphone&&d(r,{dimColor:!0,color:t.muted,children:["UK ",B(i.word.ukphone)]})]}),o(n,{marginTop:1,flexDirection:"column",children:(i.word.trans??[]).map((l,w)=>d(r,{color:t.primary,children:["\xB7 ",l]},w))}),o(n,{marginTop:1,children:o(r,{color:t.muted,children:s.word.inDict(k(x,22))})}),e[i.word.name]&&o(n,{marginTop:1,children:o(r,{color:t.error,children:s.word.mistakes(e[i.word.name].count,e[i.word.name].lastSeen.slice(0,10))})})]})}export{Z as WordLookup};
|
|
2
|
+
//# sourceMappingURL=WordLookup-LQMQVORJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/screens/WordLookup.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { Box, Text, useInput } 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 { readdir } from 'node:fs/promises';\nimport { paths } from '../../infra/paths.js';\nimport { loadLocalDictionary } from '../../infra/dict-downloader.js';\nimport { loadMistakes, type MistakeBook } from '../../domain/mistakes.js';\nimport type { Word } from '../../domain/dictionary.js';\nimport { truncateName } from '../../util/dict-name.js';\nimport { wrapPhonetic } from '../../util/text.js';\n\ntype Hit = { dictId: string; word: Word };\n\nasync function listLocalDictIds(): Promise<string[]> {\n try {\n const files = await readdir(paths.dictsDir);\n return files\n .filter((f) => f.endsWith('.json') && !f.endsWith('.meta.json'))\n .map((f) => f.replace(/\\.json$/, ''));\n } catch {\n return [];\n }\n}\n\nexport function WordLookup() {\n const nav = useNav();\n const t = useStrings();\n const [query, setQuery] = useState('');\n const [allWords, setAllWords] = useState<Hit[]>([]);\n const [book, setBook] = useState<MistakeBook>({});\n const [loading, setLoading] = useState(true);\n const [selected, setSelected] = useState(0);\n\n useEffect(() => {\n void (async () => {\n const ids = await listLocalDictIds();\n const collected: Hit[] = [];\n for (const id of ids) {\n const words = await loadLocalDictionary(id);\n if (!words) continue;\n for (const w of words) collected.push({ dictId: id, word: w });\n }\n setAllWords(collected);\n setBook(await loadMistakes());\n setLoading(false);\n })();\n }, []);\n\n const q = query.toLowerCase().trim();\n const filtered = q\n ? allWords.filter((h) => h.word.name.toLowerCase().includes(q)).slice(0, 50)\n : [];\n\n useInput((input, key) => {\n if (key.escape) {\n nav.back();\n return;\n }\n if (key.upArrow) {\n setSelected((i) => Math.max(0, i - 1));\n return;\n }\n if (key.downArrow) {\n setSelected((i) => Math.min(filtered.length - 1, i + 1));\n return;\n }\n if (key.backspace || key.delete) {\n setQuery((s) => s.slice(0, -1));\n setSelected(0);\n return;\n }\n if (input && !key.ctrl && !key.meta && input.trim().length > 0) {\n setQuery((s) => s + input);\n setSelected(0);\n }\n });\n\n if (loading) {\n return (\n <Box alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>{t.word.indexing}</Text>\n </Box>\n );\n }\n\n if (allWords.length === 0) {\n return (\n <Box flexDirection=\"column\" alignItems=\"center\" justifyContent=\"center\" width=\"100%\" height=\"100%\">\n <Text color={PALETTE.muted}>{t.word.none}</Text>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.word.pullFirst}</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 current = filtered[selected];\n\n return (\n <Box flexDirection=\"column\" paddingX={2} paddingY={1} width=\"100%\" height=\"100%\">\n <Box>\n <Text bold color={PALETTE.accent}>{t.word.title}</Text>\n <Box flexGrow={1} />\n <Text color={PALETTE.muted}>{t.word.countAcross(allWords.length)}</Text>\n </Box>\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{'> '}</Text>\n <Text color={PALETTE.text}>{query}</Text>\n <Text color={PALETTE.accent}>_</Text>\n </Box>\n\n <Box marginTop={1} flexGrow={1}>\n <Box flexDirection=\"column\" width=\"40%\">\n {filtered.map((h, i) => (\n <HitRow key={`${h.dictId}-${h.word.name}-${i}`} hit={h} active={i === selected} />\n ))}\n {filtered.length === 0 && q && (\n <Text color={PALETTE.muted}>{t.word.noMatches(query)}</Text>\n )}\n </Box>\n\n <Box flexDirection=\"column\" width=\"60%\" paddingLeft={2}>\n {current && <Detail hit={current} book={book} />}\n </Box>\n </Box>\n\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.word.footer}</Text>\n </Box>\n </Box>\n );\n}\n\nfunction HitRow({ hit, active }: { hit: Hit; active: boolean }) {\n const name = useDictName(hit.dictId);\n return (\n <Box>\n <Text color={active ? PALETTE.accent : PALETTE.muted}>{active ? '▌ ' : ' '}</Text>\n <Text bold={active} color={active ? PALETTE.text : PALETTE.muted}>\n {hit.word.name.padEnd(20)}\n </Text>\n <Text color={PALETTE.muted}>{truncateName(name, 18)}</Text>\n </Box>\n );\n}\n\nfunction Detail({ hit, book }: { hit: Hit; book: MistakeBook }) {\n const t = useStrings();\n const name = useDictName(hit.dictId);\n return (\n <>\n <Text bold color={PALETTE.text}>{hit.word.name}</Text>\n <Box marginTop={1}>\n {hit.word.usphone && (\n <Text dimColor color={PALETTE.muted}>US {wrapPhonetic(hit.word.usphone)} </Text>\n )}\n {hit.word.ukphone && (\n <Text dimColor color={PALETTE.muted}>UK {wrapPhonetic(hit.word.ukphone)}</Text>\n )}\n </Box>\n <Box marginTop={1} flexDirection=\"column\">\n {(hit.word.trans ?? []).map((tr, i) => (\n <Text key={i} color={PALETTE.primary}>· {tr}</Text>\n ))}\n </Box>\n <Box marginTop={1}>\n <Text color={PALETTE.muted}>{t.word.inDict(truncateName(name, 22))}</Text>\n </Box>\n {book[hit.word.name] && (\n <Box marginTop={1}>\n <Text color={PALETTE.error}>\n {t.word.mistakes(book[hit.word.name]!.count, book[hit.word.name]!.lastSeen.slice(0, 10))}\n </Text>\n </Box>\n )}\n </>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","Box","Text","useInput","readdir","Fragment","jsx","jsxs","listLocalDictIds","readdir","paths","f","WordLookup","nav","useNav","t","useStrings","query","setQuery","useState","allWords","setAllWords","book","setBook","loading","setLoading","selected","setSelected","useEffect","ids","collected","id","words","loadLocalDictionary","w","loadMistakes","q","filtered","h","useInput","input","key","i","s","Box","Text","PALETTE","current","HitRow","Detail","hit","active","name","useDictName","truncateName","wrapPhonetic","tr"]}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{a as L,b as R,c as I}from"./chunk-6ROGUGNX.js";import{a as C,b as k}from"./chunk-MFGIEKBU.js";import{b as $,e as z}from"./chunk-GULN5HRV.js";import{a as B,b as D,c as H}from"./chunk-UEJCQKZ2.js";import{a as w}from"./chunk-WRO5XX35.js";import{a as P,b as M,c as E,d as N,f as a}from"./chunk-VIOZNKSK.js";var _="\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H",F="\x1B[?25h\x1B[?1049l",v=!1,W=!1;function O(){if(W)return;W=!0;let e=()=>{if(v){try{process.stdout.write(F)}catch{}v=!1}};process.once("exit",e),process.once("SIGINT",()=>{e(),process.exit(130)}),process.once("SIGTERM",()=>{e(),process.exit(143)})}function Y(){v||process.stdout.isTTY&&process.env.QWERTY_NO_ALTSCREEN!=="1"&&(O(),process.stdout.write(_),v=!0)}function A(){if(v){try{process.stdout.write(F)}catch{}v=!1}}import{Suspense as ne,lazy as T,useRef as ie}from"react";import{Box as se,Text as ae,useApp as ce,useInput as ue}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 j({children:e}){let{stdout:t}=Z(),[s,r]=X(()=>({rows:t?.rows??24,cols:t?.columns??80}));return Q(()=>{Y();let n=()=>{r({rows:process.stdout.rows??24,cols:process.stdout.columns??80})};return process.stdout.on("resize",n),()=>{process.stdout.off("resize",n),A()}},[]),ee(U,{width:s.cols,height:s.rows,flexDirection:"column",children:e})}import{useState as te}from"react";import{Box as y,Text as f,useApp as re,useInput as oe}from"ink";import{jsx as g,jsxs as x}from"react/jsx-runtime";function q({cfg:e}){let[t,s]=te(0),{exit:r}=re(),n=M(),h=R(),p=N(),l=D(e.defaultDict),o=p.mainMenu.items,S=async c=>{if(!e.defaultDict){n.navigate({name:"dict",params:{pickerMode:"choose-then-practice"}});return}let d=z(await $(),e.defaultDict);if(c){I({type:"stealth",dictId:e.defaultDict,chapterIndex:d,mode:e.defaultMode}),r();return}n.navigate({name:"practice",params:{dictId:e.defaultDict,chapterIndex:d,mode:e.defaultMode,stealth:c}})},m=[{key:"p",label:o.practiceLabel,hint:e.defaultDict?o.practiceHintWith(H(l,24)):o.practiceHintNone,run:()=>{S(e.stealth==="default")}}];(e.stealth==="menu"||e.stealth==="default")&&m.push({key:"b",label:o.stealthLabel,hint:o.stealthHint,run:()=>{S(!0)}}),m.push({key:"d",label:o.dictLabel,hint:o.dictHint,run:()=>n.navigate({name:"dict"})},{key:"w",label:o.wordLabel,hint:o.wordHint,run:()=>n.navigate({name:"word"})},{key:"s",label:o.statsLabel,hint:o.statsHint,run:()=>n.navigate({name:"stats"})},{key:"c",label:o.configLabel,hint:o.configHint,run:()=>n.navigate({name:"config"})},{key:"q",label:o.quitLabel,hint:o.quitHint,run:()=>r()});let J=Math.max(...m.map(c=>w(c.label)))+4;return oe((c,d)=>{if(d.escape){r();return}if(d.upArrow&&s(u=>(u-1+m.length)%m.length),d.downArrow&&s(u=>(u+1)%m.length),d.return){m[t].run();return}if(c==="?"){n.navigate({name:"help"});return}for(let u of m)if(c===u.key){u.run();return}}),x(y,{flexDirection:"column",paddingX:2,paddingY:1,width:"100%",children:[e.stealth==="off"&&x(y,{children:[g(f,{bold:!0,color:a.accent,children:p.app.title}),x(f,{color:a.muted,children:[" \xB7 ",p.app.subtitle]})]}),g(y,{marginTop:e.stealth==="off"?2:0,flexDirection:"column",children:m.map((c,d)=>{let u=d===t,K=" ".repeat(Math.max(0,J-w(c.label)));return x(y,{children:[g(f,{color:u?a.accent:a.muted,children:u?"\u258C ":" "}),x(f,{color:u?a.accent:a.muted,children:["[",c.key,"]"]}),g(f,{children:" "}),x(f,{bold:u,color:u?a.text:a.muted,children:[c.label,K]}),g(f,{color:a.muted,children:c.hint})]},c.key)})}),g(y,{marginTop:2,children:x(f,{color:a.muted,children:[p.mainMenu.hint," \xB7 ",p.mainMenu.helpHint]})}),h.warning&&g(y,{marginTop:1,children:g(f,{color:a.warning,children:p.audio.noPlayer})})]})}import{jsx as i}from"react/jsx-runtime";var pe=T(()=>import("./PracticeScreen-BRPEUQJ6.js").then(e=>({default:e.PracticeScreen}))),le=T(()=>import("./DictBrowser-I6AEYY2I.js").then(e=>({default:e.DictBrowser}))),me=T(()=>import("./ConfigEditor-YG5K2WT2.js").then(e=>({default:e.ConfigEditor}))),de=T(()=>import("./StatsViewer-43ATH3OG.js").then(e=>({default:e.StatsViewer}))),fe=T(()=>import("./WordLookup-LQMQVORJ.js").then(e=>({default:e.WordLookup}))),he=T(()=>import("./HelpScreen-IKHDZGTG.js").then(e=>({default:e.HelpScreen})));function ge(){return i(se,{alignItems:"center",justifyContent:"center",width:"100%",height:"100%",children:i(ae,{color:a.muted,children:"\u2026"})})}function Xe({initial:e,initialCfg:t,inline:s=!1}){return i(C,{initialCfg:t,children:i(xe,{children:i(B,{children:i(L,{disabled:!t.sounds.master,children:i(P,{initial:e,children:s?i(V,{inline:!0}):i(j,{children:i(V,{})})})})})})})}function xe({children:e}){let{cfg:t}=k();return i(E,{pref:t.language,children:e})}function be(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 V({inline:e=!1}){let t=M(),{cfg:s}=k(),{exit:r}=ce(),n=ie(null);ue((o,S)=>{S.ctrl&&o==="c"&&r()});let h=t.current,p=be(h);n.current!==p&&(!e&&process.stdout.isTTY&&process.stdout.write("\x1B[2J\x1B[H"),n.current=p);let l=(()=>{switch(h.name){case"main":return i(q,{cfg:s});case"practice":return i(pe,{params:h.params});case"dict":return i(le,{params:h.params});case"config":return i(me,{});case"stats":return i(de,{});case"word":return i(fe,{});case"help":return i(he,{})}})();return i(ne,{fallback:i(ge,{}),children:l})}import b from"chalk";import Se from"boxen";function nt(){A()}function G(e,t){let s=Math.floor(e/1e3),r=Math.floor(s/60),n=s%60;return t==="zh"?r===0?`${n} \u79D2`:`${r} \u5206 ${n} \u79D2`:r===0?`${n}s`:`${r}m ${n}s`}function we(e){return e>=90?b.green:e<75?b.dim:t=>t}function ve(e,t,s){let r=[],n=[t.report.duration];e.chaptersCompleted===0?n.push(t.report.notPracticed):(n.push(t.report.practiced,t.report.chapters,t.report.words,t.report.accuracy,t.report.wpm),e.newMistakeWords>0&&n.push(t.report.newMistakes));let h=Math.max(...n.map(w)),p=o=>o+" ".repeat(Math.max(0,h-w(o))),l=(o,S)=>`${b.dim(p(o))} ${S}`;if(r.push(l(t.report.duration,G(e.totalDurationMs,s))),e.chaptersCompleted===0)r.push(b.dim(t.report.notPracticed));else{r.push(l(t.report.practiced,G(e.practiceMs,s))),r.push(l(t.report.chapters,String(e.chaptersCompleted))),r.push(l(t.report.words,String(e.wordCount)));let o=Math.round(e.accuracy*1e3)/10;r.push(l(t.report.accuracy,we(o)(`${o}%`))),r.push(l(t.report.wpm,String(e.wpm))),e.newMistakeWords>0&&r.push(l(t.report.newMistakes,b.red(String(e.newMistakeWords))))}return r.push(""),r.push(b.dim.italic(t.report.farewell)),r}function it(e,t,s){if(e.startedAt===null&&e.chaptersCompleted===0)return;let r=ve(e,t,s).join(`
|
|
2
|
+
`);console.log(Se(r,{title:b.bold.cyan(t.report.title),titleAlignment:"left",borderStyle:"round",borderColor:"gray",padding:{top:0,bottom:0,left:3,right:3},margin:{top:1,bottom:1,left:2,right:0}}))}export{Y as a,Xe as b,nt as c,it as d};
|
|
3
|
+
//# sourceMappingURL=chunk-MHLVBOJU.js.map
|