qwerty-cli 0.0.1-alpha.4 → 0.0.1-alpha.6
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/cli.js +939 -397
- package/dist/cli.js.map +1 -1
- package/package.json +1 -2
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
2
|
import { Command as Command6 } from "commander";
|
|
3
3
|
|
|
4
|
+
// package.json
|
|
5
|
+
var package_default = {
|
|
6
|
+
name: "qwerty-cli",
|
|
7
|
+
version: "0.0.1-alpha.6",
|
|
8
|
+
description: "Terminal clone of qwerty-learner: typing practice for English vocabulary, with chapters, dictation, mistake book, and audio.",
|
|
9
|
+
type: "module",
|
|
10
|
+
bin: {
|
|
11
|
+
qwerty: "bin/qwerty.mjs"
|
|
12
|
+
},
|
|
13
|
+
files: [
|
|
14
|
+
"dist",
|
|
15
|
+
"bin",
|
|
16
|
+
"assets"
|
|
17
|
+
],
|
|
18
|
+
scripts: {
|
|
19
|
+
build: "tsup",
|
|
20
|
+
dev: "tsup --watch",
|
|
21
|
+
typecheck: "tsc --noEmit",
|
|
22
|
+
test: "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"sync-registry": "tsx scripts/sync-registry.ts",
|
|
25
|
+
prepublishOnly: "pnpm build"
|
|
26
|
+
},
|
|
27
|
+
engines: {
|
|
28
|
+
node: ">=20"
|
|
29
|
+
},
|
|
30
|
+
keywords: [
|
|
31
|
+
"qwerty",
|
|
32
|
+
"typing",
|
|
33
|
+
"cli",
|
|
34
|
+
"vocabulary",
|
|
35
|
+
"ink",
|
|
36
|
+
"terminal"
|
|
37
|
+
],
|
|
38
|
+
license: "MIT",
|
|
39
|
+
dependencies: {
|
|
40
|
+
chalk: "^5.3.0",
|
|
41
|
+
commander: "^12.1.0",
|
|
42
|
+
ink: "^5.0.1",
|
|
43
|
+
"p-queue": "^8.0.1",
|
|
44
|
+
react: "^18.3.1",
|
|
45
|
+
undici: "^6.19.8",
|
|
46
|
+
zod: "^3.23.8"
|
|
47
|
+
},
|
|
48
|
+
devDependencies: {
|
|
49
|
+
"@types/node": "^20.14.10",
|
|
50
|
+
"@types/react": "^18.3.3",
|
|
51
|
+
"ts-morph": "^23.0.0",
|
|
52
|
+
tsup: "^8.2.4",
|
|
53
|
+
tsx: "^4.19.0",
|
|
54
|
+
typescript: "^5.5.4",
|
|
55
|
+
vitest: "^2.0.5"
|
|
56
|
+
},
|
|
57
|
+
pnpm: {
|
|
58
|
+
onlyBuiltDependencies: [
|
|
59
|
+
"esbuild"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
4
64
|
// src/commands/config.ts
|
|
5
65
|
import { Command } from "commander";
|
|
6
66
|
import chalk from "chalk";
|
|
@@ -89,7 +149,8 @@ var ConfigSchema = z.object({
|
|
|
89
149
|
}).default({ master: true, keystroke: true, feedback: true, keySoundName: "default" }),
|
|
90
150
|
autoplayPronunciation: z.boolean().default(true),
|
|
91
151
|
defaultMode: z.enum(["order", "dictation", "review", "random", "loop"]).default("order"),
|
|
92
|
-
defaultDict: z.string().optional()
|
|
152
|
+
defaultDict: z.string().optional(),
|
|
153
|
+
language: z.enum(["auto", "zh", "en"]).default("auto")
|
|
93
154
|
});
|
|
94
155
|
var DEFAULTS = ConfigSchema.parse({});
|
|
95
156
|
async function loadConfig() {
|
|
@@ -445,11 +506,12 @@ ${matches.length} matches`));
|
|
|
445
506
|
|
|
446
507
|
// src/commands/practice.ts
|
|
447
508
|
import { Command as Command3 } from "commander";
|
|
448
|
-
import
|
|
509
|
+
import chalk4 from "chalk";
|
|
449
510
|
import { render } from "ink";
|
|
450
511
|
import { createElement } from "react";
|
|
451
512
|
|
|
452
513
|
// src/ui/App.tsx
|
|
514
|
+
import { useRef as useRef4 } from "react";
|
|
453
515
|
import { useApp as useApp4, useInput as useInput8 } from "ink";
|
|
454
516
|
|
|
455
517
|
// src/ui/nav.tsx
|
|
@@ -480,17 +542,27 @@ function useNav() {
|
|
|
480
542
|
}
|
|
481
543
|
|
|
482
544
|
// src/ui/Fullscreen.tsx
|
|
483
|
-
import { useEffect } from "react";
|
|
484
|
-
import {
|
|
485
|
-
|
|
545
|
+
import { useEffect, useState as useState2 } from "react";
|
|
546
|
+
import { Box, useStdout } from "ink";
|
|
547
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
548
|
+
var ENTER = "\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H";
|
|
486
549
|
var LEAVE = "\x1B[?25h\x1B[?1049l";
|
|
487
550
|
function shouldUse() {
|
|
488
551
|
return Boolean(process.stdout.isTTY) && process.env.QWERTY_NO_ALTSCREEN !== "1";
|
|
489
552
|
}
|
|
490
553
|
function Fullscreen({ children }) {
|
|
554
|
+
const { stdout } = useStdout();
|
|
555
|
+
const [size, setSize] = useState2(() => ({
|
|
556
|
+
rows: stdout?.rows ?? 24,
|
|
557
|
+
cols: stdout?.columns ?? 80
|
|
558
|
+
}));
|
|
491
559
|
useEffect(() => {
|
|
492
560
|
if (!shouldUse()) return;
|
|
493
561
|
process.stdout.write(ENTER);
|
|
562
|
+
const onResize = () => {
|
|
563
|
+
setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });
|
|
564
|
+
};
|
|
565
|
+
process.stdout.on("resize", onResize);
|
|
494
566
|
const leave = () => {
|
|
495
567
|
try {
|
|
496
568
|
process.stdout.write(LEAVE);
|
|
@@ -508,14 +580,15 @@ function Fullscreen({ children }) {
|
|
|
508
580
|
process.off("SIGINT", onSignal);
|
|
509
581
|
process.off("SIGTERM", onSignal);
|
|
510
582
|
process.off("exit", leave);
|
|
583
|
+
process.stdout.off("resize", onResize);
|
|
511
584
|
leave();
|
|
512
585
|
};
|
|
513
586
|
}, []);
|
|
514
|
-
return /* @__PURE__ */ jsx2(
|
|
587
|
+
return /* @__PURE__ */ jsx2(Box, { width: size.cols, height: size.rows, flexDirection: "column", children });
|
|
515
588
|
}
|
|
516
589
|
|
|
517
590
|
// src/ui/audio-context.tsx
|
|
518
|
-
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useState as
|
|
591
|
+
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useState as useState3 } from "react";
|
|
519
592
|
|
|
520
593
|
// src/infra/audio.ts
|
|
521
594
|
import { spawn } from "child_process";
|
|
@@ -677,7 +750,7 @@ function AudioStatusProvider({
|
|
|
677
750
|
disabled,
|
|
678
751
|
children
|
|
679
752
|
}) {
|
|
680
|
-
const [status, setStatus] =
|
|
753
|
+
const [status, setStatus] = useState3({ warning: null, ready: false });
|
|
681
754
|
useEffect2(() => {
|
|
682
755
|
let cancelled = false;
|
|
683
756
|
initAudio(disabled).then(() => {
|
|
@@ -698,14 +771,14 @@ function useAudioStatus() {
|
|
|
698
771
|
}
|
|
699
772
|
|
|
700
773
|
// src/ui/app-state.tsx
|
|
701
|
-
import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useState as
|
|
774
|
+
import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useState as useState4 } from "react";
|
|
702
775
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
703
776
|
var AppStateContext = createContext3(null);
|
|
704
777
|
function AppStateProvider({
|
|
705
778
|
initialCfg,
|
|
706
779
|
children
|
|
707
780
|
}) {
|
|
708
|
-
const [cfg, setCfgState] =
|
|
781
|
+
const [cfg, setCfgState] = useState4(initialCfg);
|
|
709
782
|
const setCfg = useCallback2(async (next) => {
|
|
710
783
|
setCfgState(next);
|
|
711
784
|
await saveConfig(next);
|
|
@@ -718,14 +791,367 @@ function useAppState() {
|
|
|
718
791
|
return ctx;
|
|
719
792
|
}
|
|
720
793
|
|
|
794
|
+
// src/i18n/context.tsx
|
|
795
|
+
import { createContext as createContext4, useContext as useContext4, useMemo } from "react";
|
|
796
|
+
|
|
797
|
+
// src/i18n/strings.ts
|
|
798
|
+
var en = {
|
|
799
|
+
app: {
|
|
800
|
+
title: "qwerty",
|
|
801
|
+
subtitle: "typing practice for the terminal"
|
|
802
|
+
},
|
|
803
|
+
common: {
|
|
804
|
+
back: "back",
|
|
805
|
+
quit: "quit",
|
|
806
|
+
on: "on",
|
|
807
|
+
off: "off"
|
|
808
|
+
},
|
|
809
|
+
mainMenu: {
|
|
810
|
+
items: {
|
|
811
|
+
practiceLabel: "Practice",
|
|
812
|
+
practiceHintWith: (id) => `start ${id}`,
|
|
813
|
+
practiceHintNone: "pick a dictionary",
|
|
814
|
+
dictLabel: "Dictionaries",
|
|
815
|
+
dictHint: "browse, pull, set default",
|
|
816
|
+
wordLabel: "Word lookup",
|
|
817
|
+
wordHint: "search local dicts",
|
|
818
|
+
statsLabel: "Stats",
|
|
819
|
+
statsHint: "history & trends",
|
|
820
|
+
configLabel: "Config",
|
|
821
|
+
configHint: "edit preferences",
|
|
822
|
+
quitLabel: "Quit",
|
|
823
|
+
quitHint: "Ctrl+C also exits"
|
|
824
|
+
},
|
|
825
|
+
defaultDict: (id) => `default dict: ${id}`,
|
|
826
|
+
noDefault: "default dict: (none \u2014 pick one in Dictionaries)",
|
|
827
|
+
hint: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump"
|
|
828
|
+
},
|
|
829
|
+
dict: {
|
|
830
|
+
title: "Dictionaries",
|
|
831
|
+
loading: "loading dictionaries\u2026",
|
|
832
|
+
entries: (n) => `${n} entries`,
|
|
833
|
+
filterPrompt: (q) => `/ ${q}`,
|
|
834
|
+
local: "local \u2713",
|
|
835
|
+
notLocal: "not local",
|
|
836
|
+
defaultMark: "default \u2605",
|
|
837
|
+
tagsLabel: (tags) => `tags: ${tags}`,
|
|
838
|
+
wordsLabel: (n) => `${n} words`,
|
|
839
|
+
pulling: (id) => `pulling ${id}\u2026`,
|
|
840
|
+
removing: (id) => `removing ${id}\u2026`,
|
|
841
|
+
errorOn: (id, msg) => `error on ${id}: ${msg}`,
|
|
842
|
+
footer: "\u2191/\u2193 select \xB7 Enter set default \xB7 p practice \xB7 u pull \xB7 r remove \xB7 / filter \xB7 Esc back"
|
|
843
|
+
},
|
|
844
|
+
config: {
|
|
845
|
+
title: "Config",
|
|
846
|
+
fields: {
|
|
847
|
+
defaultDict: "default dict",
|
|
848
|
+
defaultMode: "default mode",
|
|
849
|
+
accent: "accent",
|
|
850
|
+
mirror: "mirror",
|
|
851
|
+
chapterSize: "chapter size",
|
|
852
|
+
autoplayPronunciation: "autoplay pronunciation",
|
|
853
|
+
soundsMaster: "sounds master",
|
|
854
|
+
soundsKeystroke: "sounds keystroke",
|
|
855
|
+
soundsFeedback: "sounds feedback",
|
|
856
|
+
soundsKeySound: "sounds key sound",
|
|
857
|
+
language: "language"
|
|
858
|
+
},
|
|
859
|
+
hints: {
|
|
860
|
+
editing: "type to edit \xB7 Enter save \xB7 Esc cancel",
|
|
861
|
+
bool: "space toggle \xB7 \u2191/\u2193 move \xB7 Esc back",
|
|
862
|
+
enum: "\u2190/\u2192 cycle \xB7 \u2191/\u2193 move \xB7 Esc back",
|
|
863
|
+
dictRef: "Enter pick dict \xB7 \u2191/\u2193 move \xB7 Esc back",
|
|
864
|
+
stringOrInt: "Enter edit \xB7 \u2191/\u2193 move \xB7 Esc back"
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
stats: {
|
|
868
|
+
title: "Stats",
|
|
869
|
+
loading: "loading stats\u2026",
|
|
870
|
+
none: "No practice history yet.",
|
|
871
|
+
nonePractice: "Run a practice session first.",
|
|
872
|
+
lifetime: "lifetime",
|
|
873
|
+
sessions: "sessions",
|
|
874
|
+
words: "words",
|
|
875
|
+
errors: "errors",
|
|
876
|
+
wpm: "wpm",
|
|
877
|
+
accuracy: "accuracy",
|
|
878
|
+
streak: "streak",
|
|
879
|
+
last: (n) => `last ${n} days (n / N to cycle window)`,
|
|
880
|
+
cycleWindow: "n / N cycle window \xB7 q back",
|
|
881
|
+
recent: "recent sessions",
|
|
882
|
+
topMistakes: "top mistakes",
|
|
883
|
+
footer: "n / N cycle window \xB7 q back"
|
|
884
|
+
},
|
|
885
|
+
word: {
|
|
886
|
+
title: "Word lookup",
|
|
887
|
+
indexing: "indexing local dictionaries\u2026",
|
|
888
|
+
none: "No local dictionaries.",
|
|
889
|
+
pullFirst: "Pull one in Dictionaries first.",
|
|
890
|
+
countAcross: (n) => `${n} words across local dicts`,
|
|
891
|
+
noMatches: (q) => `no matches for "${q}"`,
|
|
892
|
+
inDict: (id) => `in: ${id}`,
|
|
893
|
+
mistakes: (n, date) => `mistakes: ${n} (last ${date})`,
|
|
894
|
+
footer: "type to filter \xB7 \u2191/\u2193 select \xB7 Esc back"
|
|
895
|
+
},
|
|
896
|
+
practice: {
|
|
897
|
+
loading: "loading\u2026",
|
|
898
|
+
paused: "PAUSED",
|
|
899
|
+
chapterComplete: "CHAPTER COMPLETE",
|
|
900
|
+
chapterLabel: (c, t) => `chapter ${c}/${t}`,
|
|
901
|
+
reviewLabel: "review",
|
|
902
|
+
statusBar: {
|
|
903
|
+
mode: "mode",
|
|
904
|
+
accent: "accent"
|
|
905
|
+
},
|
|
906
|
+
modes: {
|
|
907
|
+
order: "order",
|
|
908
|
+
dictation: "dictation",
|
|
909
|
+
review: "review",
|
|
910
|
+
random: "random",
|
|
911
|
+
loop: "loop"
|
|
912
|
+
},
|
|
913
|
+
accents: {
|
|
914
|
+
us: "us",
|
|
915
|
+
uk: "uk"
|
|
916
|
+
},
|
|
917
|
+
statCards: {
|
|
918
|
+
words: "words",
|
|
919
|
+
errors: "errors",
|
|
920
|
+
wpm: "wpm",
|
|
921
|
+
accuracy: "accuracy",
|
|
922
|
+
elapsed: (t) => `elapsed ${t}`
|
|
923
|
+
},
|
|
924
|
+
footers: {
|
|
925
|
+
typing: "Ctrl+N skip \xB7 Esc pause \xB7 Tab replay \xB7 Ctrl+C quit",
|
|
926
|
+
paused: "[r] resume \xB7 [q] quit to menu",
|
|
927
|
+
summary: "[n] next chapter \xB7 [m] review mistakes \xB7 [q] back to menu"
|
|
928
|
+
},
|
|
929
|
+
errors: {
|
|
930
|
+
noMistakes: "No mistakes to review yet. Practice some chapters first.",
|
|
931
|
+
dictEmpty: (id) => `Dictionary ${id} is empty.`,
|
|
932
|
+
unknown: "Unknown error"
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
audio: {
|
|
936
|
+
noPlayer: "! No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled."
|
|
937
|
+
},
|
|
938
|
+
report: {
|
|
939
|
+
title: "Session summary",
|
|
940
|
+
duration: "duration",
|
|
941
|
+
practiced: "practiced",
|
|
942
|
+
chapters: "chapters",
|
|
943
|
+
words: "words",
|
|
944
|
+
accuracy: "accuracy",
|
|
945
|
+
wpm: "wpm",
|
|
946
|
+
newMistakes: "new mistakes",
|
|
947
|
+
farewell: "see you next time."
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
var zh = {
|
|
951
|
+
app: {
|
|
952
|
+
title: "qwerty",
|
|
953
|
+
subtitle: "\u7EC8\u7AEF\u952E\u76D8\u7EC3\u4E60"
|
|
954
|
+
},
|
|
955
|
+
common: {
|
|
956
|
+
back: "\u8FD4\u56DE",
|
|
957
|
+
quit: "\u9000\u51FA",
|
|
958
|
+
on: "\u5F00",
|
|
959
|
+
off: "\u5173"
|
|
960
|
+
},
|
|
961
|
+
mainMenu: {
|
|
962
|
+
items: {
|
|
963
|
+
practiceLabel: "\u7EC3\u4E60",
|
|
964
|
+
practiceHintWith: (id) => `\u5F00\u59CB ${id}`,
|
|
965
|
+
practiceHintNone: "\u8BF7\u5148\u9009\u8BCD\u5178",
|
|
966
|
+
dictLabel: "\u8BCD\u5178",
|
|
967
|
+
dictHint: "\u6D4F\u89C8\u3001\u4E0B\u8F7D\u3001\u8BBE\u4E3A\u9ED8\u8BA4",
|
|
968
|
+
wordLabel: "\u67E5\u8BCD",
|
|
969
|
+
wordHint: "\u5728\u672C\u5730\u8BCD\u5178\u4E2D\u641C\u7D22",
|
|
970
|
+
statsLabel: "\u7EDF\u8BA1",
|
|
971
|
+
statsHint: "\u5386\u53F2\u4E0E\u8D8B\u52BF",
|
|
972
|
+
configLabel: "\u8BBE\u7F6E",
|
|
973
|
+
configHint: "\u4FEE\u6539\u504F\u597D",
|
|
974
|
+
quitLabel: "\u9000\u51FA",
|
|
975
|
+
quitHint: "Ctrl+C \u4EA6\u53EF\u9000\u51FA"
|
|
976
|
+
},
|
|
977
|
+
defaultDict: (id) => `\u9ED8\u8BA4\u8BCD\u5178:${id}`,
|
|
978
|
+
noDefault: "\u9ED8\u8BA4\u8BCD\u5178:(\u672A\u8BBE\u7F6E \u2014 \u5728\u300C\u8BCD\u5178\u300D\u4E2D\u9009\u62E9)",
|
|
979
|
+
hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE"
|
|
980
|
+
},
|
|
981
|
+
dict: {
|
|
982
|
+
title: "\u8BCD\u5178",
|
|
983
|
+
loading: "\u52A0\u8F7D\u8BCD\u5178\u4E2D\u2026",
|
|
984
|
+
entries: (n) => `${n} \u90E8\u8BCD\u5178`,
|
|
985
|
+
filterPrompt: (q) => `/ ${q}`,
|
|
986
|
+
local: "\u5DF2\u4E0B\u8F7D \u2713",
|
|
987
|
+
notLocal: "\u672A\u4E0B\u8F7D",
|
|
988
|
+
defaultMark: "\u9ED8\u8BA4 \u2605",
|
|
989
|
+
tagsLabel: (tags) => `\u6807\u7B7E:${tags}`,
|
|
990
|
+
wordsLabel: (n) => `${n} \u8BCD`,
|
|
991
|
+
pulling: (id) => `\u62C9\u53D6 ${id} \u4E2D\u2026`,
|
|
992
|
+
removing: (id) => `\u5220\u9664 ${id} \u4E2D\u2026`,
|
|
993
|
+
errorOn: (id, msg) => `${id} \u51FA\u9519:${msg}`,
|
|
994
|
+
footer: "\u2191/\u2193 \u9009\u62E9 \xB7 Enter \u8BBE\u4E3A\u9ED8\u8BA4 \xB7 p \u5F00\u59CB\u7EC3\u4E60 \xB7 u \u62C9\u53D6 \xB7 r \u5220\u9664 \xB7 / \u8FC7\u6EE4 \xB7 Esc \u8FD4\u56DE"
|
|
995
|
+
},
|
|
996
|
+
config: {
|
|
997
|
+
title: "\u8BBE\u7F6E",
|
|
998
|
+
fields: {
|
|
999
|
+
defaultDict: "\u9ED8\u8BA4\u8BCD\u5178",
|
|
1000
|
+
defaultMode: "\u9ED8\u8BA4\u6A21\u5F0F",
|
|
1001
|
+
accent: "\u53D1\u97F3",
|
|
1002
|
+
mirror: "\u955C\u50CF\u6E90",
|
|
1003
|
+
chapterSize: "\u7AE0\u8282\u5355\u8BCD\u6570",
|
|
1004
|
+
autoplayPronunciation: "\u81EA\u52A8\u64AD\u653E\u53D1\u97F3",
|
|
1005
|
+
soundsMaster: "\u97F3\u6548\u603B\u5F00\u5173",
|
|
1006
|
+
soundsKeystroke: "\u6309\u952E\u97F3",
|
|
1007
|
+
soundsFeedback: "\u53CD\u9988\u97F3",
|
|
1008
|
+
soundsKeySound: "\u6309\u952E\u97F3\u8272",
|
|
1009
|
+
language: "\u8BED\u8A00"
|
|
1010
|
+
},
|
|
1011
|
+
hints: {
|
|
1012
|
+
editing: "\u8F93\u5165\u4FEE\u6539 \xB7 Enter \u4FDD\u5B58 \xB7 Esc \u53D6\u6D88",
|
|
1013
|
+
bool: "\u7A7A\u683C\u5207\u6362 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",
|
|
1014
|
+
enum: "\u2190/\u2192 \u5207\u6362 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",
|
|
1015
|
+
dictRef: "Enter \u9009\u8BCD\u5178 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",
|
|
1016
|
+
stringOrInt: "Enter \u7F16\u8F91 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE"
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
stats: {
|
|
1020
|
+
title: "\u7EDF\u8BA1",
|
|
1021
|
+
loading: "\u52A0\u8F7D\u7EDF\u8BA1\u4E2D\u2026",
|
|
1022
|
+
none: "\u8FD8\u6CA1\u6709\u7EC3\u4E60\u8BB0\u5F55\u3002",
|
|
1023
|
+
nonePractice: "\u5148\u6765\u4E00\u6B21\u7EC3\u4E60\u5427\u3002",
|
|
1024
|
+
lifetime: "\u7D2F\u8BA1",
|
|
1025
|
+
sessions: "\u4F1A\u8BDD",
|
|
1026
|
+
words: "\u8BCD\u6570",
|
|
1027
|
+
errors: "\u9519\u8BEF",
|
|
1028
|
+
wpm: "\u901F\u5EA6",
|
|
1029
|
+
accuracy: "\u51C6\u786E\u7387",
|
|
1030
|
+
streak: "\u8FDE\u7EED\u5929\u6570",
|
|
1031
|
+
last: (n) => `\u6700\u8FD1 ${n} \u5929 (n / N \u5207\u6362\u7A97\u53E3)`,
|
|
1032
|
+
cycleWindow: "n / N \u5207\u6362\u7A97\u53E3 \xB7 q \u8FD4\u56DE",
|
|
1033
|
+
recent: "\u6700\u8FD1\u4F1A\u8BDD",
|
|
1034
|
+
topMistakes: "\u9AD8\u9891\u9519\u8BCD",
|
|
1035
|
+
footer: "n / N \u5207\u6362\u7A97\u53E3 \xB7 q \u8FD4\u56DE"
|
|
1036
|
+
},
|
|
1037
|
+
word: {
|
|
1038
|
+
title: "\u67E5\u8BCD",
|
|
1039
|
+
indexing: "\u7D22\u5F15\u672C\u5730\u8BCD\u5178\u4E2D\u2026",
|
|
1040
|
+
none: "\u6CA1\u6709\u672C\u5730\u8BCD\u5178\u3002",
|
|
1041
|
+
pullFirst: "\u5148\u5728\u300C\u8BCD\u5178\u300D\u4E2D\u62C9\u53D6\u4E00\u90E8\u3002",
|
|
1042
|
+
countAcross: (n) => `\u672C\u5730\u8BCD\u5178\u5171 ${n} \u8BCD`,
|
|
1043
|
+
noMatches: (q) => `\u6CA1\u6709\u5339\u914D\u300C${q}\u300D\u7684\u8BCD`,
|
|
1044
|
+
inDict: (id) => `\u6765\u6E90:${id}`,
|
|
1045
|
+
mistakes: (n, date) => `\u9519\u8FC7 ${n} \u6B21 (\u6700\u8FD1 ${date})`,
|
|
1046
|
+
footer: "\u8F93\u5165\u8FC7\u6EE4 \xB7 \u2191/\u2193 \u9009\u62E9 \xB7 Esc \u8FD4\u56DE"
|
|
1047
|
+
},
|
|
1048
|
+
practice: {
|
|
1049
|
+
loading: "\u52A0\u8F7D\u4E2D\u2026",
|
|
1050
|
+
paused: "\u5DF2\u6682\u505C",
|
|
1051
|
+
chapterComplete: "\u672C\u7AE0\u5B8C\u6210",
|
|
1052
|
+
chapterLabel: (c, t) => `\u7B2C ${c}/${t} \u7AE0`,
|
|
1053
|
+
reviewLabel: "\u590D\u4E60",
|
|
1054
|
+
statusBar: {
|
|
1055
|
+
mode: "\u6A21\u5F0F",
|
|
1056
|
+
accent: "\u53D1\u97F3"
|
|
1057
|
+
},
|
|
1058
|
+
modes: {
|
|
1059
|
+
order: "\u987A\u5E8F",
|
|
1060
|
+
dictation: "\u9ED8\u5199",
|
|
1061
|
+
review: "\u590D\u4E60",
|
|
1062
|
+
random: "\u4E71\u5E8F",
|
|
1063
|
+
loop: "\u5FAA\u73AF"
|
|
1064
|
+
},
|
|
1065
|
+
accents: {
|
|
1066
|
+
us: "\u7F8E",
|
|
1067
|
+
uk: "\u82F1"
|
|
1068
|
+
},
|
|
1069
|
+
statCards: {
|
|
1070
|
+
words: "\u8BCD\u6570",
|
|
1071
|
+
errors: "\u9519\u8BEF",
|
|
1072
|
+
wpm: "\u901F\u5EA6",
|
|
1073
|
+
accuracy: "\u51C6\u786E\u7387",
|
|
1074
|
+
elapsed: (t) => `\u8017\u65F6 ${t}`
|
|
1075
|
+
},
|
|
1076
|
+
footers: {
|
|
1077
|
+
typing: "Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD \xB7 Ctrl+C \u9000\u51FA",
|
|
1078
|
+
paused: "[r] \u7EE7\u7EED \xB7 [q] \u8FD4\u56DE\u83DC\u5355",
|
|
1079
|
+
summary: "[n] \u4E0B\u4E00\u7AE0 \xB7 [m] \u590D\u4E60\u9519\u8BCD \xB7 [q] \u8FD4\u56DE\u83DC\u5355"
|
|
1080
|
+
},
|
|
1081
|
+
errors: {
|
|
1082
|
+
noMistakes: "\u9519\u8BCD\u672C\u662F\u7A7A\u7684\u3002\u5148\u7EC3\u4E60\u51E0\u7AE0\u5427\u3002",
|
|
1083
|
+
dictEmpty: (id) => `\u8BCD\u5178 ${id} \u662F\u7A7A\u7684\u3002`,
|
|
1084
|
+
unknown: "\u672A\u77E5\u9519\u8BEF"
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
audio: {
|
|
1088
|
+
noPlayer: "! \u672A\u5728 PATH \u4E2D\u627E\u5230\u97F3\u9891\u64AD\u653E\u5668(\u5C1D\u8BD5 afplay/ffplay/mpg123/paplay/aplay/powershell)\u3002\u97F3\u6548\u5DF2\u7981\u7528\u3002"
|
|
1089
|
+
},
|
|
1090
|
+
report: {
|
|
1091
|
+
title: "\u672C\u6B21\u4F1A\u8BDD",
|
|
1092
|
+
duration: "\u603B\u65F6\u957F",
|
|
1093
|
+
practiced: "\u7EC3\u4E60\u7528\u65F6",
|
|
1094
|
+
chapters: "\u5B8C\u6210\u7AE0\u8282",
|
|
1095
|
+
words: "\u8BCD\u6570",
|
|
1096
|
+
accuracy: "\u51C6\u786E\u7387",
|
|
1097
|
+
wpm: "\u901F\u5EA6",
|
|
1098
|
+
newMistakes: "\u65B0\u9519\u8BCD",
|
|
1099
|
+
farewell: "\u4E0B\u6B21\u89C1\u3002"
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// src/i18n/locale.ts
|
|
1104
|
+
function pickFromString(s) {
|
|
1105
|
+
if (!s) return null;
|
|
1106
|
+
const lower = s.toLowerCase();
|
|
1107
|
+
if (lower.startsWith("zh")) return "zh";
|
|
1108
|
+
if (lower.startsWith("en")) return "en";
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
function detectLocale(pref) {
|
|
1112
|
+
if (pref === "zh" || pref === "en") return pref;
|
|
1113
|
+
const env = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE;
|
|
1114
|
+
const fromEnv = pickFromString(env);
|
|
1115
|
+
if (fromEnv) return fromEnv;
|
|
1116
|
+
try {
|
|
1117
|
+
const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1118
|
+
const fromIntl = pickFromString(intlLocale);
|
|
1119
|
+
if (fromIntl) return fromIntl;
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
return "en";
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/i18n/context.tsx
|
|
1126
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1127
|
+
var StringsContext = createContext4(null);
|
|
1128
|
+
function StringsProvider({
|
|
1129
|
+
pref,
|
|
1130
|
+
children
|
|
1131
|
+
}) {
|
|
1132
|
+
const value = useMemo(() => {
|
|
1133
|
+
const lang = detectLocale(pref);
|
|
1134
|
+
return { lang, t: lang === "zh" ? zh : en };
|
|
1135
|
+
}, [pref]);
|
|
1136
|
+
return /* @__PURE__ */ jsx5(StringsContext.Provider, { value, children });
|
|
1137
|
+
}
|
|
1138
|
+
function useStrings() {
|
|
1139
|
+
const ctx = useContext4(StringsContext);
|
|
1140
|
+
if (!ctx) throw new Error("useStrings must be used inside StringsProvider");
|
|
1141
|
+
return ctx.t;
|
|
1142
|
+
}
|
|
1143
|
+
function pickStrings(pref) {
|
|
1144
|
+
const lang = detectLocale(pref);
|
|
1145
|
+
return { lang, t: lang === "zh" ? zh : en };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
721
1148
|
// src/ui/screens/MainMenu.tsx
|
|
722
|
-
import { useState as
|
|
723
|
-
import { Box as
|
|
1149
|
+
import { useState as useState5 } from "react";
|
|
1150
|
+
import { Box as Box3, Text as Text2, useApp, useInput } from "ink";
|
|
724
1151
|
|
|
725
1152
|
// src/ui/components/BigWord.tsx
|
|
726
|
-
import { Box, Text, useStdout } from "ink";
|
|
727
|
-
import
|
|
728
|
-
import { jsx as jsx5, jsxs } from "react/jsx-runtime";
|
|
1153
|
+
import { Box as Box2, Text, useStdout as useStdout2 } from "ink";
|
|
1154
|
+
import { jsx as jsx6, jsxs } from "react/jsx-runtime";
|
|
729
1155
|
var PALETTE = {
|
|
730
1156
|
accent: "#5eead4",
|
|
731
1157
|
muted: "#6b7280",
|
|
@@ -736,42 +1162,58 @@ var PALETTE = {
|
|
|
736
1162
|
error: "#f87171"
|
|
737
1163
|
};
|
|
738
1164
|
function BigWord({ target, typed, error = false, hideTarget = false }) {
|
|
739
|
-
const { stdout } =
|
|
1165
|
+
const { stdout } = useStdout2();
|
|
740
1166
|
const cols = stdout?.columns ?? 80;
|
|
741
1167
|
const chars = [...target];
|
|
742
1168
|
const typedChars = [...typed];
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
return /* @__PURE__ */ jsx5(Box, { justifyContent: "center", children: chars.map((ch, i) => {
|
|
746
|
-
const isTyped = i < typedChars.length;
|
|
747
|
-
const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
|
|
748
|
-
const color = isTyped ? PALETTE.accent : error ? PALETTE.error : PALETTE.muted;
|
|
749
|
-
return /* @__PURE__ */ jsxs(Text, { bold: isTyped, color, underline: !isTyped && error, children: [
|
|
750
|
-
display,
|
|
751
|
-
" "
|
|
752
|
-
] }, i);
|
|
753
|
-
}) });
|
|
754
|
-
}
|
|
755
|
-
return /* @__PURE__ */ jsx5(Box, { justifyContent: "center", flexDirection: "row", children: chars.map((ch, i) => {
|
|
1169
|
+
const sep = cols < 60 ? " " : " ";
|
|
1170
|
+
return /* @__PURE__ */ jsx6(Box2, { justifyContent: "center", children: chars.map((ch, i) => {
|
|
756
1171
|
const isTyped = i < typedChars.length;
|
|
757
1172
|
const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
|
|
758
1173
|
const color = isTyped ? PALETTE.accent : error ? PALETTE.error : PALETTE.muted;
|
|
759
|
-
return /* @__PURE__ */
|
|
1174
|
+
return /* @__PURE__ */ jsxs(Text, { bold: true, color, underline: !isTyped && error, children: [
|
|
1175
|
+
display,
|
|
1176
|
+
i < chars.length - 1 ? sep : ""
|
|
1177
|
+
] }, i);
|
|
760
1178
|
}) });
|
|
761
1179
|
}
|
|
1180
|
+
function BoldSpaced({ text, color = PALETTE.text }) {
|
|
1181
|
+
const chars = [...text];
|
|
1182
|
+
return /* @__PURE__ */ jsx6(Box2, { children: chars.map((ch, i) => /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
|
|
1183
|
+
ch,
|
|
1184
|
+
i < chars.length - 1 ? " " : ""
|
|
1185
|
+
] }, i)) });
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/util/text.ts
|
|
1189
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
1190
|
+
function stripAnsi(s) {
|
|
1191
|
+
return s.replace(ANSI_RE, "");
|
|
1192
|
+
}
|
|
1193
|
+
function visibleWidth2(s) {
|
|
1194
|
+
const plain = stripAnsi(s);
|
|
1195
|
+
let w = 0;
|
|
1196
|
+
for (const ch of plain) {
|
|
1197
|
+
const code = ch.codePointAt(0);
|
|
1198
|
+
w += code > 11904 && code < 64256 ? 2 : 1;
|
|
1199
|
+
}
|
|
1200
|
+
return w;
|
|
1201
|
+
}
|
|
762
1202
|
|
|
763
1203
|
// src/ui/screens/MainMenu.tsx
|
|
764
|
-
import { jsx as
|
|
1204
|
+
import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
765
1205
|
function MainMenu({ cfg }) {
|
|
766
|
-
const [selected, setSelected] =
|
|
1206
|
+
const [selected, setSelected] = useState5(0);
|
|
767
1207
|
const { exit } = useApp();
|
|
768
1208
|
const nav = useNav();
|
|
769
1209
|
const audio = useAudioStatus();
|
|
1210
|
+
const t = useStrings();
|
|
1211
|
+
const m = t.mainMenu.items;
|
|
770
1212
|
const items = [
|
|
771
1213
|
{
|
|
772
1214
|
key: "p",
|
|
773
|
-
label:
|
|
774
|
-
hint: cfg.defaultDict ?
|
|
1215
|
+
label: m.practiceLabel,
|
|
1216
|
+
hint: cfg.defaultDict ? m.practiceHintWith(cfg.defaultDict) : m.practiceHintNone,
|
|
775
1217
|
run: () => {
|
|
776
1218
|
if (cfg.defaultDict) {
|
|
777
1219
|
nav.navigate({
|
|
@@ -783,12 +1225,13 @@ function MainMenu({ cfg }) {
|
|
|
783
1225
|
}
|
|
784
1226
|
}
|
|
785
1227
|
},
|
|
786
|
-
{ key: "d", label:
|
|
787
|
-
{ key: "w", label:
|
|
788
|
-
{ key: "s", label:
|
|
789
|
-
{ key: "c", label:
|
|
790
|
-
{ key: "q", label:
|
|
1228
|
+
{ key: "d", label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: "dict" }) },
|
|
1229
|
+
{ key: "w", label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: "word" }) },
|
|
1230
|
+
{ key: "s", label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: "stats" }) },
|
|
1231
|
+
{ key: "c", label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: "config" }) },
|
|
1232
|
+
{ key: "q", label: m.quitLabel, hint: m.quitHint, run: () => exit() }
|
|
791
1233
|
];
|
|
1234
|
+
const labelW = Math.max(...items.map((it) => visibleWidth2(it.label))) + 4;
|
|
792
1235
|
useInput((input, key) => {
|
|
793
1236
|
if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);
|
|
794
1237
|
if (key.downArrow) setSelected((i) => (i + 1) % items.length);
|
|
@@ -803,41 +1246,41 @@ function MainMenu({ cfg }) {
|
|
|
803
1246
|
}
|
|
804
1247
|
}
|
|
805
1248
|
});
|
|
806
|
-
return /* @__PURE__ */ jsxs2(
|
|
807
|
-
/* @__PURE__ */ jsxs2(
|
|
808
|
-
/* @__PURE__ */
|
|
809
|
-
/* @__PURE__ */
|
|
1249
|
+
return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", children: [
|
|
1250
|
+
/* @__PURE__ */ jsxs2(Box3, { children: [
|
|
1251
|
+
/* @__PURE__ */ jsx7(Text2, { bold: true, color: PALETTE.accent, children: t.app.title }),
|
|
1252
|
+
/* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
|
|
1253
|
+
" \xB7 ",
|
|
1254
|
+
t.app.subtitle
|
|
1255
|
+
] })
|
|
810
1256
|
] }),
|
|
811
|
-
/* @__PURE__ */
|
|
1257
|
+
/* @__PURE__ */ jsx7(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
|
|
812
1258
|
const active = i === selected;
|
|
813
|
-
|
|
814
|
-
|
|
1259
|
+
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(it.label)));
|
|
1260
|
+
return /* @__PURE__ */ jsxs2(Box3, { children: [
|
|
1261
|
+
/* @__PURE__ */ jsx7(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
815
1262
|
/* @__PURE__ */ jsxs2(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
|
|
816
1263
|
"[",
|
|
817
1264
|
it.key,
|
|
818
1265
|
"]"
|
|
819
1266
|
] }),
|
|
820
|
-
/* @__PURE__ */
|
|
821
|
-
/* @__PURE__ */
|
|
822
|
-
|
|
1267
|
+
/* @__PURE__ */ jsx7(Text2, { children: " " }),
|
|
1268
|
+
/* @__PURE__ */ jsxs2(Text2, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
|
|
1269
|
+
it.label,
|
|
1270
|
+
pad
|
|
1271
|
+
] }),
|
|
1272
|
+
/* @__PURE__ */ jsx7(Text2, { color: PALETTE.muted, children: it.hint })
|
|
823
1273
|
] }, it.key);
|
|
824
1274
|
}) }),
|
|
825
|
-
/* @__PURE__ */
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
] }) }),
|
|
829
|
-
/* @__PURE__ */ jsx6(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text2, { color: PALETTE.muted, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump" }) }),
|
|
830
|
-
audio.warning && /* @__PURE__ */ jsx6(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.warning, children: [
|
|
831
|
-
"! ",
|
|
832
|
-
audio.warning
|
|
833
|
-
] }) })
|
|
1275
|
+
/* @__PURE__ */ jsx7(Box3, { marginTop: 2, children: /* @__PURE__ */ jsx7(Text2, { color: PALETTE.muted, children: cfg.defaultDict ? t.mainMenu.defaultDict(cfg.defaultDict) : t.mainMenu.noDefault }) }),
|
|
1276
|
+
/* @__PURE__ */ jsx7(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text2, { color: PALETTE.muted, children: t.mainMenu.hint }) }),
|
|
1277
|
+
audio.warning && /* @__PURE__ */ jsx7(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text2, { color: PALETTE.warning, children: t.audio.noPlayer }) })
|
|
834
1278
|
] });
|
|
835
1279
|
}
|
|
836
1280
|
|
|
837
1281
|
// src/ui/screens/PracticeScreen.tsx
|
|
838
|
-
import { useState as
|
|
839
|
-
import { Box as
|
|
840
|
-
import BigText2 from "ink-big-text";
|
|
1282
|
+
import { useState as useState7, useEffect as useEffect5, useRef as useRef3 } from "react";
|
|
1283
|
+
import { Box as Box4, Text as Text3, useApp as useApp3, useInput as useInput3 } from "ink";
|
|
841
1284
|
|
|
842
1285
|
// src/util/shuffle.ts
|
|
843
1286
|
function shuffle(arr, rng = Math.random) {
|
|
@@ -881,25 +1324,25 @@ function buildPlaylist(chapter, mode, seed) {
|
|
|
881
1324
|
function initialState(target) {
|
|
882
1325
|
return { target, typed: "", errorsThisWord: 0 };
|
|
883
1326
|
}
|
|
884
|
-
function reduce(
|
|
1327
|
+
function reduce(state2, ev) {
|
|
885
1328
|
switch (ev.type) {
|
|
886
1329
|
case "reset":
|
|
887
|
-
return { state: { ...
|
|
1330
|
+
return { state: { ...state2, typed: "" }, effect: "none" };
|
|
888
1331
|
case "backspace": {
|
|
889
|
-
if (
|
|
890
|
-
return { state: { ...
|
|
1332
|
+
if (state2.typed.length === 0) return { state: state2, effect: "none" };
|
|
1333
|
+
return { state: { ...state2, typed: state2.typed.slice(0, -1) }, effect: "none" };
|
|
891
1334
|
}
|
|
892
1335
|
case "char": {
|
|
893
|
-
const candidate =
|
|
894
|
-
const targetUpToCandidate = [...
|
|
1336
|
+
const candidate = state2.typed + ev.ch;
|
|
1337
|
+
const targetUpToCandidate = [...state2.target].slice(0, [...candidate].length).join("");
|
|
895
1338
|
if (candidate === targetUpToCandidate) {
|
|
896
|
-
if (candidate.length ===
|
|
897
|
-
return { state: { ...
|
|
1339
|
+
if (candidate.length === state2.target.length) {
|
|
1340
|
+
return { state: { ...state2, typed: candidate }, effect: "correct" };
|
|
898
1341
|
}
|
|
899
|
-
return { state: { ...
|
|
1342
|
+
return { state: { ...state2, typed: candidate }, effect: "progress" };
|
|
900
1343
|
}
|
|
901
1344
|
return {
|
|
902
|
-
state: { ...
|
|
1345
|
+
state: { ...state2, typed: "", errorsThisWord: state2.errorsThisWord + 1 },
|
|
903
1346
|
effect: "wrong"
|
|
904
1347
|
};
|
|
905
1348
|
}
|
|
@@ -921,11 +1364,11 @@ function startSession(playlist, now = Date.now()) {
|
|
|
921
1364
|
}
|
|
922
1365
|
function feedSession(session, ev, now = Date.now()) {
|
|
923
1366
|
if (!session.current) return { session, effect: "none" };
|
|
924
|
-
const { state, effect } = reduce(session.current.input, ev);
|
|
1367
|
+
const { state: state2, effect } = reduce(session.current.input, ev);
|
|
925
1368
|
if (effect === "correct") {
|
|
926
1369
|
const finished = {
|
|
927
|
-
word:
|
|
928
|
-
errors:
|
|
1370
|
+
word: state2.target,
|
|
1371
|
+
errors: state2.errorsThisWord,
|
|
929
1372
|
durationMs: now - session.current.wordStartedAt
|
|
930
1373
|
};
|
|
931
1374
|
const nextIndex = session.current.wordIndex + 1;
|
|
@@ -952,7 +1395,7 @@ function feedSession(session, ev, now = Date.now()) {
|
|
|
952
1395
|
return {
|
|
953
1396
|
session: {
|
|
954
1397
|
...session,
|
|
955
|
-
current: { ...session.current, input:
|
|
1398
|
+
current: { ...session.current, input: state2 }
|
|
956
1399
|
},
|
|
957
1400
|
effect
|
|
958
1401
|
};
|
|
@@ -1036,24 +1479,24 @@ function topN(book, n) {
|
|
|
1036
1479
|
}
|
|
1037
1480
|
|
|
1038
1481
|
// src/ui/hooks/useWordLoop.ts
|
|
1039
|
-
import { useEffect as useEffect3, useReducer, useRef, useState as
|
|
1482
|
+
import { useEffect as useEffect3, useReducer, useRef, useState as useState6 } from "react";
|
|
1040
1483
|
import { useInput as useInput2, useApp as useApp2 } from "ink";
|
|
1041
|
-
function reducer(
|
|
1484
|
+
function reducer(state2, action) {
|
|
1042
1485
|
if (action.type === "start") {
|
|
1043
1486
|
return { session: startSession(action.playlist, action.now), lastEffect: null };
|
|
1044
1487
|
}
|
|
1045
1488
|
if (action.type === "skip") {
|
|
1046
|
-
const r = skipSession(
|
|
1489
|
+
const r = skipSession(state2.session, action.now);
|
|
1047
1490
|
return { session: r.session, lastEffect: r.effect };
|
|
1048
1491
|
}
|
|
1049
1492
|
if (action.type === "event") {
|
|
1050
1493
|
if (action.key.backspace || action.key.delete) {
|
|
1051
|
-
const r = feedSession(
|
|
1494
|
+
const r = feedSession(state2.session, { type: "backspace" }, action.now);
|
|
1052
1495
|
return { session: r.session, lastEffect: r.effect };
|
|
1053
1496
|
}
|
|
1054
|
-
if (action.input.length === 0) return
|
|
1055
|
-
let session =
|
|
1056
|
-
let lastEffect =
|
|
1497
|
+
if (action.input.length === 0) return state2;
|
|
1498
|
+
let session = state2.session;
|
|
1499
|
+
let lastEffect = state2.lastEffect;
|
|
1057
1500
|
for (const c of action.input) {
|
|
1058
1501
|
const r = feedSession(session, { type: "char", ch: c }, action.now);
|
|
1059
1502
|
session = r.session;
|
|
@@ -1062,15 +1505,15 @@ function reducer(state, action) {
|
|
|
1062
1505
|
}
|
|
1063
1506
|
return { session, lastEffect };
|
|
1064
1507
|
}
|
|
1065
|
-
return
|
|
1508
|
+
return state2;
|
|
1066
1509
|
}
|
|
1067
1510
|
function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled = true }) {
|
|
1068
|
-
const [
|
|
1511
|
+
const [state2, dispatch] = useReducer(reducer, void 0, () => ({
|
|
1069
1512
|
session: startSession(playlist, Date.now()),
|
|
1070
1513
|
lastEffect: null
|
|
1071
1514
|
}));
|
|
1072
1515
|
const completedRef = useRef(false);
|
|
1073
|
-
const [tick, setTick] =
|
|
1516
|
+
const [tick, setTick] = useState6(0);
|
|
1074
1517
|
const { exit } = useApp2();
|
|
1075
1518
|
useInput2(
|
|
1076
1519
|
(input, key) => {
|
|
@@ -1103,17 +1546,17 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1103
1546
|
{ isActive: enabled }
|
|
1104
1547
|
);
|
|
1105
1548
|
useEffect3(() => {
|
|
1106
|
-
if (
|
|
1549
|
+
if (state2.session.finishedAt !== null && !completedRef.current) {
|
|
1107
1550
|
completedRef.current = true;
|
|
1108
|
-
onComplete(
|
|
1551
|
+
onComplete(state2.session);
|
|
1109
1552
|
}
|
|
1110
|
-
}, [
|
|
1553
|
+
}, [state2.session, onComplete]);
|
|
1111
1554
|
useEffect3(() => {
|
|
1112
|
-
if (
|
|
1555
|
+
if (state2.session.finishedAt !== null) return;
|
|
1113
1556
|
const id = setInterval(() => setTick((t) => t + 1), 1e3);
|
|
1114
1557
|
return () => clearInterval(id);
|
|
1115
|
-
}, [
|
|
1116
|
-
return { session:
|
|
1558
|
+
}, [state2.session.finishedAt]);
|
|
1559
|
+
return { session: state2.session, lastEffect: state2.lastEffect, tick };
|
|
1117
1560
|
}
|
|
1118
1561
|
|
|
1119
1562
|
// src/ui/hooks/useAudio.ts
|
|
@@ -1223,6 +1666,43 @@ function dailyBuckets(sessions, days, now = /* @__PURE__ */ new Date()) {
|
|
|
1223
1666
|
return out;
|
|
1224
1667
|
}
|
|
1225
1668
|
|
|
1669
|
+
// src/infra/session-tracker.ts
|
|
1670
|
+
var state = {
|
|
1671
|
+
startedAt: null,
|
|
1672
|
+
chapters: []
|
|
1673
|
+
};
|
|
1674
|
+
function start(now = Date.now()) {
|
|
1675
|
+
if (state.startedAt === null) state.startedAt = now;
|
|
1676
|
+
}
|
|
1677
|
+
function addChapter(entry) {
|
|
1678
|
+
if (state.startedAt === null) state.startedAt = Date.now();
|
|
1679
|
+
state.chapters.push(entry);
|
|
1680
|
+
}
|
|
1681
|
+
function report(now = Date.now()) {
|
|
1682
|
+
const chapters = state.chapters;
|
|
1683
|
+
const wordCount = chapters.reduce((a, c) => a + c.wordCount, 0);
|
|
1684
|
+
const errors = chapters.reduce((a, c) => a + c.errors, 0);
|
|
1685
|
+
const practiceMs = chapters.reduce((a, c) => a + c.durationMs, 0);
|
|
1686
|
+
const minutes = practiceMs / 6e4;
|
|
1687
|
+
const wpm = minutes > 0 ? Math.round(wordCount / minutes * 10) / 10 : 0;
|
|
1688
|
+
const errorWordSet = /* @__PURE__ */ new Set();
|
|
1689
|
+
for (const c of chapters) {
|
|
1690
|
+
for (const w of Object.keys(c.perWordErrors)) errorWordSet.add(w);
|
|
1691
|
+
}
|
|
1692
|
+
const accuracy2 = wordCount === 0 ? 1 : Math.max(0, (wordCount - errorWordSet.size) / wordCount);
|
|
1693
|
+
return {
|
|
1694
|
+
startedAt: state.startedAt,
|
|
1695
|
+
totalDurationMs: state.startedAt === null ? 0 : now - state.startedAt,
|
|
1696
|
+
chaptersCompleted: chapters.length,
|
|
1697
|
+
wordCount,
|
|
1698
|
+
errors,
|
|
1699
|
+
wpm,
|
|
1700
|
+
accuracy: accuracy2,
|
|
1701
|
+
newMistakeWords: errorWordSet.size,
|
|
1702
|
+
practiceMs
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1226
1706
|
// src/ui/hooks/useSessionPersistence.ts
|
|
1227
1707
|
function useSessionPersistence(meta) {
|
|
1228
1708
|
return useCallback3(
|
|
@@ -1238,6 +1718,15 @@ function useSessionPersistence(meta) {
|
|
|
1238
1718
|
perWordErrors: summary.perWordErrors
|
|
1239
1719
|
};
|
|
1240
1720
|
await appendSession(rec);
|
|
1721
|
+
addChapter({
|
|
1722
|
+
dictId: meta.dictId,
|
|
1723
|
+
chapterIndex: meta.chapterIndex,
|
|
1724
|
+
mode: meta.mode,
|
|
1725
|
+
wordCount: summary.wordCount,
|
|
1726
|
+
errors: summary.errors,
|
|
1727
|
+
durationMs: summary.durationMs,
|
|
1728
|
+
perWordErrors: summary.perWordErrors
|
|
1729
|
+
});
|
|
1241
1730
|
const dirty = Object.entries(summary.perWordErrors).filter(([, n]) => n > 0);
|
|
1242
1731
|
if (dirty.length === 0) return;
|
|
1243
1732
|
let book = await loadMistakes();
|
|
@@ -1249,14 +1738,14 @@ function useSessionPersistence(meta) {
|
|
|
1249
1738
|
}
|
|
1250
1739
|
|
|
1251
1740
|
// src/ui/screens/PracticeScreen.tsx
|
|
1252
|
-
import { jsx as
|
|
1741
|
+
import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1253
1742
|
function PracticeScreen({ params }) {
|
|
1254
1743
|
const { dictId, chapterIndex, mode } = params;
|
|
1255
1744
|
const { cfg } = useAppState();
|
|
1256
|
-
const
|
|
1257
|
-
const [phase, setPhase] =
|
|
1258
|
-
const [loaded, setLoaded] =
|
|
1259
|
-
const [errorMsg, setErrorMsg] =
|
|
1745
|
+
const t = useStrings();
|
|
1746
|
+
const [phase, setPhase] = useState7("loading");
|
|
1747
|
+
const [loaded, setLoaded] = useState7(null);
|
|
1748
|
+
const [errorMsg, setErrorMsg] = useState7(null);
|
|
1260
1749
|
useEffect5(() => {
|
|
1261
1750
|
let cancelled = false;
|
|
1262
1751
|
setPhase("loading");
|
|
@@ -1271,7 +1760,7 @@ function PracticeScreen({ params }) {
|
|
|
1271
1760
|
if (cancelled) return;
|
|
1272
1761
|
const reviewWords = words.filter((w) => book[w.name]?.count).slice(0, cfg.chapterSize);
|
|
1273
1762
|
if (reviewWords.length === 0) {
|
|
1274
|
-
setErrorMsg(
|
|
1763
|
+
setErrorMsg(t.practice.errors.noMistakes);
|
|
1275
1764
|
setPhase("error");
|
|
1276
1765
|
return;
|
|
1277
1766
|
}
|
|
@@ -1281,7 +1770,7 @@ function PracticeScreen({ params }) {
|
|
|
1281
1770
|
}
|
|
1282
1771
|
const chapters = chunkChapters(words, cfg.chapterSize);
|
|
1283
1772
|
if (chapters.length === 0) {
|
|
1284
|
-
setErrorMsg(
|
|
1773
|
+
setErrorMsg(t.practice.errors.dictEmpty(dictId));
|
|
1285
1774
|
setPhase("error");
|
|
1286
1775
|
return;
|
|
1287
1776
|
}
|
|
@@ -1298,15 +1787,15 @@ function PracticeScreen({ params }) {
|
|
|
1298
1787
|
return () => {
|
|
1299
1788
|
cancelled = true;
|
|
1300
1789
|
};
|
|
1301
|
-
}, [dictId, chapterIndex, mode, cfg.chapterSize]);
|
|
1790
|
+
}, [dictId, chapterIndex, mode, cfg.chapterSize, t]);
|
|
1302
1791
|
if (phase === "loading") {
|
|
1303
|
-
return /* @__PURE__ */
|
|
1792
|
+
return /* @__PURE__ */ jsx8(Centered, { text: t.practice.loading, color: PALETTE.muted });
|
|
1304
1793
|
}
|
|
1305
1794
|
if (phase === "error") {
|
|
1306
|
-
return /* @__PURE__ */
|
|
1795
|
+
return /* @__PURE__ */ jsx8(ErrorView, { msg: errorMsg ?? t.practice.errors.unknown });
|
|
1307
1796
|
}
|
|
1308
1797
|
if (!loaded) return null;
|
|
1309
|
-
return /* @__PURE__ */
|
|
1798
|
+
return /* @__PURE__ */ jsx8(
|
|
1310
1799
|
PracticeRunner,
|
|
1311
1800
|
{
|
|
1312
1801
|
params,
|
|
@@ -1407,9 +1896,9 @@ function PracticeRunner({
|
|
|
1407
1896
|
},
|
|
1408
1897
|
{ isActive: phase === "summary" }
|
|
1409
1898
|
);
|
|
1410
|
-
if (phase === "paused") return /* @__PURE__ */
|
|
1899
|
+
if (phase === "paused") return /* @__PURE__ */ jsx8(PausedView, {});
|
|
1411
1900
|
if (phase === "summary") {
|
|
1412
|
-
return /* @__PURE__ */
|
|
1901
|
+
return /* @__PURE__ */ jsx8(
|
|
1413
1902
|
SummaryView,
|
|
1414
1903
|
{
|
|
1415
1904
|
dictId,
|
|
@@ -1427,7 +1916,7 @@ function PracticeRunner({
|
|
|
1427
1916
|
const errors = session.results.reduce((a, r) => a + r.errors, 0);
|
|
1428
1917
|
const minutes = elapsedMs / 6e4;
|
|
1429
1918
|
const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
|
|
1430
|
-
return /* @__PURE__ */
|
|
1919
|
+
return /* @__PURE__ */ jsx8(
|
|
1431
1920
|
TypingLayout,
|
|
1432
1921
|
{
|
|
1433
1922
|
dictId,
|
|
@@ -1461,9 +1950,10 @@ function fmtTime(ms) {
|
|
|
1461
1950
|
return `${m}:${String(s).padStart(2, "0")}`;
|
|
1462
1951
|
}
|
|
1463
1952
|
function TypingLayout(props) {
|
|
1953
|
+
const t = useStrings();
|
|
1464
1954
|
const progressFrac = props.total === 0 ? 0 : props.completed / props.total;
|
|
1465
|
-
return /* @__PURE__ */ jsxs3(
|
|
1466
|
-
/* @__PURE__ */
|
|
1955
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
1956
|
+
/* @__PURE__ */ jsx8(
|
|
1467
1957
|
StatusBar,
|
|
1468
1958
|
{
|
|
1469
1959
|
dictId: props.dictId,
|
|
@@ -1476,8 +1966,8 @@ function TypingLayout(props) {
|
|
|
1476
1966
|
elapsedMs: props.elapsedMs
|
|
1477
1967
|
}
|
|
1478
1968
|
),
|
|
1479
|
-
/* @__PURE__ */ jsxs3(
|
|
1480
|
-
/* @__PURE__ */
|
|
1969
|
+
/* @__PURE__ */ jsxs3(Box4, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
|
|
1970
|
+
/* @__PURE__ */ jsx8(
|
|
1481
1971
|
BigWord,
|
|
1482
1972
|
{
|
|
1483
1973
|
target: props.target,
|
|
@@ -1486,12 +1976,12 @@ function TypingLayout(props) {
|
|
|
1486
1976
|
hideTarget: props.hideTarget
|
|
1487
1977
|
}
|
|
1488
1978
|
),
|
|
1489
|
-
props.phonetic && /* @__PURE__ */
|
|
1490
|
-
props.translation.length > 0 && /* @__PURE__ */
|
|
1979
|
+
props.phonetic && /* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text3, { italic: true, color: PALETTE.muted, children: props.phonetic }) }),
|
|
1980
|
+
props.translation.length > 0 && /* @__PURE__ */ jsx8(Box4, { marginTop: 1, flexDirection: "column", alignItems: "center", children: props.translation.slice(0, 2).map((tr, i) => /* @__PURE__ */ jsx8(Text3, { color: PALETTE.primary, children: tr }, i)) })
|
|
1491
1981
|
] }),
|
|
1492
|
-
/* @__PURE__ */ jsxs3(
|
|
1493
|
-
/* @__PURE__ */
|
|
1494
|
-
/* @__PURE__ */
|
|
1982
|
+
/* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
|
|
1983
|
+
/* @__PURE__ */ jsx8(ProgressBar, { frac: progressFrac }),
|
|
1984
|
+
/* @__PURE__ */ jsx8(Box4, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: PALETTE.muted, children: [
|
|
1495
1985
|
props.completed,
|
|
1496
1986
|
"/",
|
|
1497
1987
|
props.total,
|
|
@@ -1499,21 +1989,27 @@ function TypingLayout(props) {
|
|
|
1499
1989
|
fmtTime(props.elapsedMs),
|
|
1500
1990
|
" \xB7 ",
|
|
1501
1991
|
props.wpm,
|
|
1502
|
-
"
|
|
1992
|
+
" ",
|
|
1993
|
+
t.practice.statCards.wpm,
|
|
1994
|
+
" \xB7 ",
|
|
1503
1995
|
props.errors,
|
|
1504
|
-
"
|
|
1996
|
+
" ",
|
|
1997
|
+
t.practice.statCards.errors
|
|
1505
1998
|
] }) }),
|
|
1506
|
-
/* @__PURE__ */
|
|
1999
|
+
/* @__PURE__ */ jsx8(Box4, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.footers.typing }) })
|
|
1507
2000
|
] })
|
|
1508
2001
|
] });
|
|
1509
2002
|
}
|
|
1510
2003
|
function StatusBar(props) {
|
|
1511
|
-
const
|
|
2004
|
+
const t = useStrings();
|
|
2005
|
+
const modeName = t.practice.modes[props.mode];
|
|
2006
|
+
const accentName = t.practice.accents[props.accent];
|
|
2007
|
+
const left = props.mode === "review" ? `${props.dictId} \xB7 ${t.practice.reviewLabel} \xB7 ${accentName}` : `${props.dictId} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName} \xB7 ${accentName}`;
|
|
1512
2008
|
const right = `${props.completed}/${props.total} \xB7 ${fmtTime(props.elapsedMs)}`;
|
|
1513
|
-
return /* @__PURE__ */ jsxs3(
|
|
1514
|
-
/* @__PURE__ */
|
|
1515
|
-
/* @__PURE__ */
|
|
1516
|
-
/* @__PURE__ */
|
|
2009
|
+
return /* @__PURE__ */ jsxs3(Box4, { children: [
|
|
2010
|
+
/* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: left }),
|
|
2011
|
+
/* @__PURE__ */ jsx8(Box4, { flexGrow: 1 }),
|
|
2012
|
+
/* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: right })
|
|
1517
2013
|
] });
|
|
1518
2014
|
}
|
|
1519
2015
|
function ProgressBar({ frac }) {
|
|
@@ -1521,22 +2017,27 @@ function ProgressBar({ frac }) {
|
|
|
1521
2017
|
const width = Math.max(20, Math.min(72, cols - 16));
|
|
1522
2018
|
const filled = Math.round(width * Math.max(0, Math.min(1, frac)));
|
|
1523
2019
|
const empty = width - filled;
|
|
1524
|
-
return /* @__PURE__ */ jsxs3(
|
|
1525
|
-
/* @__PURE__ */
|
|
1526
|
-
/* @__PURE__ */
|
|
2020
|
+
return /* @__PURE__ */ jsxs3(Box4, { justifyContent: "center", children: [
|
|
2021
|
+
/* @__PURE__ */ jsx8(Text3, { color: PALETTE.accent, children: "\u2501".repeat(filled) }),
|
|
2022
|
+
/* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: "\u2500".repeat(empty) })
|
|
1527
2023
|
] });
|
|
1528
2024
|
}
|
|
1529
2025
|
function PausedView() {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
/* @__PURE__ */
|
|
2026
|
+
const t = useStrings();
|
|
2027
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2028
|
+
/* @__PURE__ */ jsx8(BoldSpaced, { text: t.practice.paused, color: PALETTE.warning }),
|
|
2029
|
+
/* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.footers.paused }) })
|
|
1533
2030
|
] });
|
|
1534
2031
|
}
|
|
1535
2032
|
function ErrorView({ msg }) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
/* @__PURE__ */
|
|
1539
|
-
/* @__PURE__ */
|
|
2033
|
+
const t = useStrings();
|
|
2034
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2035
|
+
/* @__PURE__ */ jsx8(Text3, { color: PALETTE.error, children: msg }),
|
|
2036
|
+
/* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: PALETTE.muted, children: [
|
|
2037
|
+
"[q] ",
|
|
2038
|
+
t.common.back
|
|
2039
|
+
] }) }),
|
|
2040
|
+
/* @__PURE__ */ jsx8(BackKey, {})
|
|
1540
2041
|
] });
|
|
1541
2042
|
}
|
|
1542
2043
|
function BackKey() {
|
|
@@ -1547,7 +2048,7 @@ function BackKey() {
|
|
|
1547
2048
|
return null;
|
|
1548
2049
|
}
|
|
1549
2050
|
function Centered({ text, color }) {
|
|
1550
|
-
return /* @__PURE__ */
|
|
2051
|
+
return /* @__PURE__ */ jsx8(Box4, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx8(Text3, { color, children: text }) });
|
|
1551
2052
|
}
|
|
1552
2053
|
function SummaryView(props) {
|
|
1553
2054
|
const { summary } = props;
|
|
@@ -1556,52 +2057,52 @@ function SummaryView(props) {
|
|
|
1556
2057
|
const errorWords = Object.keys(summary.perWordErrors).length;
|
|
1557
2058
|
const acc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - errorWords) / summary.wordCount);
|
|
1558
2059
|
const accPct = Math.round(acc * 1e3) / 10;
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
/* @__PURE__ */
|
|
1564
|
-
|
|
1565
|
-
|
|
2060
|
+
const t = useStrings();
|
|
2061
|
+
const modeName = t.practice.modes[props.mode];
|
|
2062
|
+
const subtitle = props.mode === "review" ? `${props.dictId} \xB7 ${t.practice.reviewLabel}` : `${props.dictId} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName}`;
|
|
2063
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 1, width: "100%", height: "100%", children: [
|
|
2064
|
+
/* @__PURE__ */ jsx8(BoldSpaced, { text: t.practice.chapterComplete, color: PALETTE.success }),
|
|
2065
|
+
/* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: subtitle }) }),
|
|
2066
|
+
/* @__PURE__ */ jsxs3(Box4, { marginTop: 3, flexDirection: "row", justifyContent: "center", children: [
|
|
2067
|
+
/* @__PURE__ */ jsx8(StatCard, { label: t.practice.statCards.words, value: String(summary.wordCount), color: PALETTE.text }),
|
|
2068
|
+
/* @__PURE__ */ jsx8(
|
|
1566
2069
|
StatCard,
|
|
1567
2070
|
{
|
|
1568
|
-
label:
|
|
2071
|
+
label: t.practice.statCards.errors,
|
|
1569
2072
|
value: String(summary.errors),
|
|
1570
2073
|
color: summary.errors > 0 ? PALETTE.error : PALETTE.muted
|
|
1571
2074
|
}
|
|
1572
2075
|
),
|
|
1573
|
-
/* @__PURE__ */
|
|
1574
|
-
/* @__PURE__ */
|
|
2076
|
+
/* @__PURE__ */ jsx8(StatCard, { label: t.practice.statCards.wpm, value: String(wpm), color: PALETTE.accent }),
|
|
2077
|
+
/* @__PURE__ */ jsx8(StatCard, { label: t.practice.statCards.accuracy, value: `${accPct}%`, color: PALETTE.accent })
|
|
1575
2078
|
] }),
|
|
1576
|
-
/* @__PURE__ */
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
] }) }),
|
|
1580
|
-
/* @__PURE__ */ jsx7(Box3, { flexGrow: 1 }),
|
|
1581
|
-
/* @__PURE__ */ jsx7(Box3, { marginTop: 2, children: /* @__PURE__ */ jsx7(Text3, { color: PALETTE.muted, children: "[n] next chapter \xB7 [m] review mistakes \xB7 [q] back to menu" }) })
|
|
2079
|
+
/* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.statCards.elapsed(fmtTime(summary.durationMs)) }) }),
|
|
2080
|
+
/* @__PURE__ */ jsx8(Box4, { flexGrow: 1 }),
|
|
2081
|
+
/* @__PURE__ */ jsx8(Box4, { marginTop: 2, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.footers.summary }) })
|
|
1582
2082
|
] });
|
|
1583
2083
|
}
|
|
1584
2084
|
function StatCard({ label, value, color }) {
|
|
1585
|
-
return /* @__PURE__ */ jsxs3(
|
|
1586
|
-
/* @__PURE__ */
|
|
1587
|
-
/* @__PURE__ */
|
|
2085
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", alignItems: "center", marginX: 3, children: [
|
|
2086
|
+
/* @__PURE__ */ jsx8(Text3, { bold: true, color, children: value }),
|
|
2087
|
+
/* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: label })
|
|
1588
2088
|
] });
|
|
1589
2089
|
}
|
|
1590
2090
|
|
|
1591
2091
|
// src/ui/screens/DictBrowser.tsx
|
|
1592
|
-
import { useEffect as useEffect6, useState as
|
|
1593
|
-
import { Box as
|
|
1594
|
-
import { Fragment
|
|
2092
|
+
import { useEffect as useEffect6, useState as useState8 } from "react";
|
|
2093
|
+
import { Box as Box5, Text as Text4, useInput as useInput4 } from "ink";
|
|
2094
|
+
import { Fragment, jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1595
2095
|
function DictBrowser({ params }) {
|
|
1596
2096
|
const nav = useNav();
|
|
1597
2097
|
const { cfg, setCfg } = useAppState();
|
|
1598
|
-
const
|
|
1599
|
-
const [
|
|
1600
|
-
const [
|
|
1601
|
-
const [
|
|
1602
|
-
const [
|
|
1603
|
-
const [
|
|
1604
|
-
const [
|
|
2098
|
+
const t = useStrings();
|
|
2099
|
+
const [rows, setRows] = useState8([]);
|
|
2100
|
+
const [loading, setLoading] = useState8(true);
|
|
2101
|
+
const [selected, setSelected] = useState8(0);
|
|
2102
|
+
const [filter, setFilter] = useState8("");
|
|
2103
|
+
const [filterFocus, setFilterFocus] = useState8(false);
|
|
2104
|
+
const [pending, setPending] = useState8(null);
|
|
2105
|
+
const [tick, setTick] = useState8(0);
|
|
1605
2106
|
const refresh = async () => {
|
|
1606
2107
|
const reg = await loadRegistry();
|
|
1607
2108
|
const flagged = await Promise.all(
|
|
@@ -1669,7 +2170,7 @@ function DictBrowser({ params }) {
|
|
|
1669
2170
|
try {
|
|
1670
2171
|
await removeDictionary(current.entry.id);
|
|
1671
2172
|
setPending(null);
|
|
1672
|
-
setTick((
|
|
2173
|
+
setTick((n) => n + 1);
|
|
1673
2174
|
} catch (err) {
|
|
1674
2175
|
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
1675
2176
|
}
|
|
@@ -1682,7 +2183,7 @@ function DictBrowser({ params }) {
|
|
|
1682
2183
|
try {
|
|
1683
2184
|
await pullDictionary(current.entry.id);
|
|
1684
2185
|
setPending(null);
|
|
1685
|
-
setTick((
|
|
2186
|
+
setTick((n) => n + 1);
|
|
1686
2187
|
} catch (err) {
|
|
1687
2188
|
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
1688
2189
|
}
|
|
@@ -1690,88 +2191,67 @@ function DictBrowser({ params }) {
|
|
|
1690
2191
|
}
|
|
1691
2192
|
});
|
|
1692
2193
|
if (loading) {
|
|
1693
|
-
return /* @__PURE__ */
|
|
2194
|
+
return /* @__PURE__ */ jsx9(Box5, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: t.dict.loading }) });
|
|
1694
2195
|
}
|
|
1695
|
-
return /* @__PURE__ */ jsxs4(
|
|
1696
|
-
/* @__PURE__ */ jsxs4(
|
|
1697
|
-
/* @__PURE__ */
|
|
1698
|
-
/* @__PURE__ */
|
|
1699
|
-
/* @__PURE__ */
|
|
2196
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2197
|
+
/* @__PURE__ */ jsxs4(Box5, { children: [
|
|
2198
|
+
/* @__PURE__ */ jsx9(Text4, { bold: true, color: PALETTE.accent, children: t.dict.title }),
|
|
2199
|
+
/* @__PURE__ */ jsx9(Box5, { flexGrow: 1 }),
|
|
2200
|
+
/* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: filterFocus ? `/ ${filter}_` : filter ? t.dict.filterPrompt(filter) : t.dict.entries(filtered.length) })
|
|
1700
2201
|
] }),
|
|
1701
|
-
/* @__PURE__ */ jsxs4(
|
|
1702
|
-
/* @__PURE__ */
|
|
2202
|
+
/* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexGrow: 1, children: [
|
|
2203
|
+
/* @__PURE__ */ jsx9(Box5, { flexDirection: "column", width: "75%", paddingRight: 1, children: filtered.slice(Math.max(0, safeSelected - 8), safeSelected + 16).map((row, vi) => {
|
|
1703
2204
|
const i = Math.max(0, safeSelected - 8) + vi;
|
|
1704
2205
|
const active = i === safeSelected;
|
|
1705
2206
|
const isDefault = cfg.defaultDict === row.entry.id;
|
|
1706
|
-
return /* @__PURE__ */ jsxs4(
|
|
1707
|
-
/* @__PURE__ */
|
|
1708
|
-
/* @__PURE__ */
|
|
1709
|
-
/* @__PURE__ */
|
|
1710
|
-
/* @__PURE__ */
|
|
1711
|
-
/* @__PURE__ */
|
|
1712
|
-
/* @__PURE__ */ jsx8(Text4, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: row.entry.id.slice(0, 14).padEnd(15) }),
|
|
1713
|
-
/* @__PURE__ */ jsx8(Text4, { color: PALETTE.muted, children: String(row.entry.length).padStart(5) })
|
|
2207
|
+
return /* @__PURE__ */ jsxs4(Box5, { children: [
|
|
2208
|
+
/* @__PURE__ */ jsx9(Box5, { width: 2, children: /* @__PURE__ */ jsx9(Text4, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }) }),
|
|
2209
|
+
/* @__PURE__ */ jsx9(Box5, { width: 2, children: /* @__PURE__ */ jsx9(Text4, { color: row.local ? PALETTE.accent : PALETTE.muted, children: row.local ? "\u25CF" : "\u25CB" }) }),
|
|
2210
|
+
/* @__PURE__ */ jsx9(Box5, { width: 2, children: /* @__PURE__ */ jsx9(Text4, { color: isDefault ? PALETTE.success : PALETTE.muted, children: isDefault ? "\u2605" : " " }) }),
|
|
2211
|
+
/* @__PURE__ */ jsx9(Box5, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text4, { bold: active, color: active ? PALETTE.text : PALETTE.muted, wrap: "wrap", children: row.entry.name }) }),
|
|
2212
|
+
/* @__PURE__ */ jsx9(Box5, { width: 6, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: String(row.entry.length).padStart(5) }) })
|
|
1714
2213
|
] }, row.entry.id);
|
|
1715
2214
|
}) }),
|
|
1716
|
-
/* @__PURE__ */
|
|
1717
|
-
/* @__PURE__ */
|
|
1718
|
-
/* @__PURE__ */
|
|
1719
|
-
/* @__PURE__ */
|
|
2215
|
+
/* @__PURE__ */ jsx9(Box5, { flexDirection: "column", width: "25%", paddingLeft: 1, children: current && /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
2216
|
+
/* @__PURE__ */ jsx9(Text4, { bold: true, color: PALETTE.text, wrap: "wrap", children: current.entry.name }),
|
|
2217
|
+
/* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: current.entry.id }),
|
|
2218
|
+
/* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, wrap: "wrap", children: [
|
|
1720
2219
|
current.entry.language,
|
|
1721
2220
|
" \xB7 ",
|
|
1722
|
-
current.entry.category
|
|
1723
|
-
" \xB7 ",
|
|
1724
|
-
current.entry.length,
|
|
1725
|
-
" words"
|
|
1726
|
-
] }) }),
|
|
1727
|
-
/* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text4, { color: PALETTE.primary, children: current.entry.description || "(no description)" }) }),
|
|
1728
|
-
current.entry.tags.length > 0 && /* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
|
|
1729
|
-
"tags: ",
|
|
1730
|
-
current.entry.tags.join(", ")
|
|
2221
|
+
current.entry.category
|
|
1731
2222
|
] }) }),
|
|
1732
|
-
/* @__PURE__ */
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2223
|
+
/* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: t.dict.wordsLabel(current.entry.length) }) }),
|
|
2224
|
+
current.entry.description && /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.primary, wrap: "wrap", children: current.entry.description }) }),
|
|
2225
|
+
current.entry.tags.length > 0 && /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, wrap: "wrap", children: t.dict.tagsLabel(current.entry.tags.join(", ")) }) }),
|
|
2226
|
+
/* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: current.local ? PALETTE.accent : PALETTE.muted, children: current.local ? t.dict.local : t.dict.notLocal }) }),
|
|
2227
|
+
cfg.defaultDict === current.entry.id && /* @__PURE__ */ jsx9(Box5, { children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.success, children: t.dict.defaultMark }) })
|
|
1736
2228
|
] }) })
|
|
1737
2229
|
] }),
|
|
1738
|
-
pending && /* @__PURE__ */ jsxs4(
|
|
1739
|
-
pending.kind === "pulling" && /* @__PURE__ */
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
"\u2026"
|
|
1743
|
-
] }),
|
|
1744
|
-
pending.kind === "removing" && /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.warning, children: [
|
|
1745
|
-
"removing ",
|
|
1746
|
-
pending.id,
|
|
1747
|
-
"\u2026"
|
|
1748
|
-
] }),
|
|
1749
|
-
pending.kind === "error" && /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.error, children: [
|
|
1750
|
-
"error on ",
|
|
1751
|
-
pending.id,
|
|
1752
|
-
": ",
|
|
1753
|
-
pending.msg
|
|
1754
|
-
] })
|
|
2230
|
+
pending && /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, children: [
|
|
2231
|
+
pending.kind === "pulling" && /* @__PURE__ */ jsx9(Text4, { color: PALETTE.warning, children: t.dict.pulling(pending.id) }),
|
|
2232
|
+
pending.kind === "removing" && /* @__PURE__ */ jsx9(Text4, { color: PALETTE.warning, children: t.dict.removing(pending.id) }),
|
|
2233
|
+
pending.kind === "error" && /* @__PURE__ */ jsx9(Text4, { color: PALETTE.error, children: t.dict.errorOn(pending.id, pending.msg) })
|
|
1755
2234
|
] }),
|
|
1756
|
-
/* @__PURE__ */
|
|
2235
|
+
/* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: t.dict.footer }) })
|
|
1757
2236
|
] });
|
|
1758
2237
|
}
|
|
1759
2238
|
|
|
1760
2239
|
// src/ui/screens/ConfigEditor.tsx
|
|
1761
|
-
import { useState as
|
|
1762
|
-
import { Box as
|
|
1763
|
-
import { jsx as
|
|
2240
|
+
import { useState as useState9 } from "react";
|
|
2241
|
+
import { Box as Box6, Text as Text5, useInput as useInput5 } from "ink";
|
|
2242
|
+
import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1764
2243
|
var FIELDS = [
|
|
1765
|
-
{ kind: "dictRef", path: "defaultDict",
|
|
1766
|
-
{ kind: "enum", path: "defaultMode",
|
|
1767
|
-
{ kind: "enum", path: "accent",
|
|
1768
|
-
{ kind: "enum", path: "
|
|
1769
|
-
{ kind: "
|
|
1770
|
-
{ kind: "
|
|
1771
|
-
{ kind: "bool", path: "
|
|
1772
|
-
{ kind: "bool", path: "sounds.
|
|
1773
|
-
{ kind: "bool", path: "sounds.
|
|
1774
|
-
{ kind: "
|
|
2244
|
+
{ kind: "dictRef", path: "defaultDict", labelKey: "defaultDict" },
|
|
2245
|
+
{ kind: "enum", path: "defaultMode", labelKey: "defaultMode", options: ["order", "dictation", "review", "random", "loop"] },
|
|
2246
|
+
{ kind: "enum", path: "accent", labelKey: "accent", options: ["us", "uk"] },
|
|
2247
|
+
{ kind: "enum", path: "language", labelKey: "language", options: ["auto", "zh", "en"] },
|
|
2248
|
+
{ kind: "enum", path: "mirror", labelKey: "mirror", options: ["jsdelivr", "github"] },
|
|
2249
|
+
{ kind: "int", path: "chapterSize", labelKey: "chapterSize", min: 1, max: 200 },
|
|
2250
|
+
{ kind: "bool", path: "autoplayPronunciation", labelKey: "autoplayPronunciation" },
|
|
2251
|
+
{ kind: "bool", path: "sounds.master", labelKey: "soundsMaster" },
|
|
2252
|
+
{ kind: "bool", path: "sounds.keystroke", labelKey: "soundsKeystroke" },
|
|
2253
|
+
{ kind: "bool", path: "sounds.feedback", labelKey: "soundsFeedback" },
|
|
2254
|
+
{ kind: "string", path: "sounds.keySoundName", labelKey: "soundsKeySound" }
|
|
1775
2255
|
];
|
|
1776
2256
|
function getByPath2(cfg, path) {
|
|
1777
2257
|
return path.split(".").reduce((acc, k) => {
|
|
@@ -1782,10 +2262,11 @@ function getByPath2(cfg, path) {
|
|
|
1782
2262
|
function ConfigEditor() {
|
|
1783
2263
|
const nav = useNav();
|
|
1784
2264
|
const { cfg, setCfg } = useAppState();
|
|
1785
|
-
const
|
|
1786
|
-
const [
|
|
1787
|
-
const [
|
|
1788
|
-
const [
|
|
2265
|
+
const t = useStrings();
|
|
2266
|
+
const [selected, setSelected] = useState9(0);
|
|
2267
|
+
const [editing, setEditing] = useState9(false);
|
|
2268
|
+
const [draft, setDraft] = useState9("");
|
|
2269
|
+
const [error, setError] = useState9(null);
|
|
1789
2270
|
const field = FIELDS[selected];
|
|
1790
2271
|
const currentValue = getByPath2(cfg, field.path);
|
|
1791
2272
|
const commit = async (raw) => {
|
|
@@ -1868,50 +2349,57 @@ function ConfigEditor() {
|
|
|
1868
2349
|
setError(null);
|
|
1869
2350
|
}
|
|
1870
2351
|
});
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
/* @__PURE__ */
|
|
2352
|
+
const labelW = Math.max(...FIELDS.map((f) => visibleWidth2(t.config.fields[f.labelKey]))) + 4;
|
|
2353
|
+
return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2354
|
+
/* @__PURE__ */ jsx10(Text5, { bold: true, color: PALETTE.accent, children: t.config.title }),
|
|
2355
|
+
/* @__PURE__ */ jsx10(Box6, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: FIELDS.map((f, i) => {
|
|
1874
2356
|
const active = i === selected;
|
|
1875
2357
|
const value = getByPath2(cfg, f.path);
|
|
1876
|
-
const display = renderValue(f, value, active && editing ? draft : null);
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
/* @__PURE__ */
|
|
2358
|
+
const display = renderValue(f, value, active && editing ? draft : null, t);
|
|
2359
|
+
const label = t.config.fields[f.labelKey];
|
|
2360
|
+
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
|
|
2361
|
+
return /* @__PURE__ */ jsxs5(Box6, { children: [
|
|
2362
|
+
/* @__PURE__ */ jsx10(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2363
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
|
|
2364
|
+
label,
|
|
2365
|
+
pad
|
|
2366
|
+
] }),
|
|
2367
|
+
/* @__PURE__ */ jsx10(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: display })
|
|
1881
2368
|
] }, f.path);
|
|
1882
2369
|
}) }),
|
|
1883
|
-
error && /* @__PURE__ */
|
|
2370
|
+
error && /* @__PURE__ */ jsx10(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: PALETTE.error, children: [
|
|
1884
2371
|
"! ",
|
|
1885
2372
|
error
|
|
1886
2373
|
] }) }),
|
|
1887
|
-
/* @__PURE__ */
|
|
2374
|
+
/* @__PURE__ */ jsx10(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text5, { color: PALETTE.muted, children: hintFor(field, editing, t) }) })
|
|
1888
2375
|
] });
|
|
1889
2376
|
}
|
|
1890
|
-
function renderValue(field, value, draft) {
|
|
2377
|
+
function renderValue(field, value, draft, t) {
|
|
1891
2378
|
if (draft !== null) return `${draft}_`;
|
|
1892
|
-
if (field.kind === "bool") return value ?
|
|
1893
|
-
if (field.kind === "dictRef") return String(value ?? "
|
|
2379
|
+
if (field.kind === "bool") return value ? `\u2713 ${t.common.on}` : `\u2717 ${t.common.off}`;
|
|
2380
|
+
if (field.kind === "dictRef") return String(value ?? "\u2014");
|
|
1894
2381
|
if (field.kind === "enum") return `< ${value} >`;
|
|
1895
2382
|
return String(value ?? "");
|
|
1896
2383
|
}
|
|
1897
|
-
function hintFor(field, editing) {
|
|
1898
|
-
if (editing) return
|
|
1899
|
-
if (field.kind === "bool") return
|
|
1900
|
-
if (field.kind === "enum") return
|
|
1901
|
-
if (field.kind === "dictRef") return
|
|
1902
|
-
return
|
|
2384
|
+
function hintFor(field, editing, t) {
|
|
2385
|
+
if (editing) return t.config.hints.editing;
|
|
2386
|
+
if (field.kind === "bool") return t.config.hints.bool;
|
|
2387
|
+
if (field.kind === "enum") return t.config.hints.enum;
|
|
2388
|
+
if (field.kind === "dictRef") return t.config.hints.dictRef;
|
|
2389
|
+
return t.config.hints.stringOrInt;
|
|
1903
2390
|
}
|
|
1904
2391
|
|
|
1905
2392
|
// src/ui/screens/StatsViewer.tsx
|
|
1906
|
-
import { useEffect as useEffect7, useState as
|
|
1907
|
-
import { Box as
|
|
1908
|
-
import { jsx as
|
|
2393
|
+
import { useEffect as useEffect7, useState as useState10 } from "react";
|
|
2394
|
+
import { Box as Box7, Text as Text6, useInput as useInput6 } from "ink";
|
|
2395
|
+
import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1909
2396
|
var DAY_WINDOWS = [7, 14, 30, 90];
|
|
1910
2397
|
function StatsViewer() {
|
|
1911
2398
|
const nav = useNav();
|
|
1912
|
-
const
|
|
1913
|
-
const [
|
|
1914
|
-
const [
|
|
2399
|
+
const t = useStrings();
|
|
2400
|
+
const [sessions, setSessions] = useState10(null);
|
|
2401
|
+
const [book, setBook] = useState10(null);
|
|
2402
|
+
const [windowIdx, setWindowIdx] = useState10(1);
|
|
1915
2403
|
useEffect7(() => {
|
|
1916
2404
|
void (async () => {
|
|
1917
2405
|
const [s, b] = await Promise.all([loadSessions(), loadMistakes()]);
|
|
@@ -1928,13 +2416,16 @@ function StatsViewer() {
|
|
|
1928
2416
|
if (input === "N") setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);
|
|
1929
2417
|
});
|
|
1930
2418
|
if (!sessions || !book) {
|
|
1931
|
-
return /* @__PURE__ */
|
|
2419
|
+
return /* @__PURE__ */ jsx11(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.loading }) });
|
|
1932
2420
|
}
|
|
1933
2421
|
if (sessions.length === 0) {
|
|
1934
|
-
return /* @__PURE__ */ jsxs6(
|
|
1935
|
-
/* @__PURE__ */
|
|
1936
|
-
/* @__PURE__ */
|
|
1937
|
-
/* @__PURE__ */
|
|
2422
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2423
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.none }),
|
|
2424
|
+
/* @__PURE__ */ jsx11(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.nonePractice }) }),
|
|
2425
|
+
/* @__PURE__ */ jsx11(Box7, { marginTop: 2, children: /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2426
|
+
"[q] ",
|
|
2427
|
+
t.common.back
|
|
2428
|
+
] }) })
|
|
1938
2429
|
] });
|
|
1939
2430
|
}
|
|
1940
2431
|
const days = DAY_WINDOWS[windowIdx];
|
|
@@ -1951,53 +2442,49 @@ function StatsViewer() {
|
|
|
1951
2442
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
1952
2443
|
const recent = sessions.slice(-5).reverse();
|
|
1953
2444
|
const top = topN(book, 8);
|
|
1954
|
-
return /* @__PURE__ */ jsxs6(
|
|
1955
|
-
/* @__PURE__ */
|
|
1956
|
-
/* @__PURE__ */ jsxs6(
|
|
1957
|
-
/* @__PURE__ */
|
|
1958
|
-
/* @__PURE__ */ jsxs6(
|
|
1959
|
-
/* @__PURE__ */
|
|
1960
|
-
/* @__PURE__ */
|
|
1961
|
-
/* @__PURE__ */
|
|
1962
|
-
/* @__PURE__ */
|
|
1963
|
-
/* @__PURE__ */
|
|
1964
|
-
/* @__PURE__ */
|
|
2445
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2446
|
+
/* @__PURE__ */ jsx11(Text6, { bold: true, color: PALETTE.accent, children: t.stats.title }),
|
|
2447
|
+
/* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
2448
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.lifetime }),
|
|
2449
|
+
/* @__PURE__ */ jsxs6(Box7, { marginTop: 1, children: [
|
|
2450
|
+
/* @__PURE__ */ jsx11(Stat, { label: t.stats.sessions, value: String(sessions.length) }),
|
|
2451
|
+
/* @__PURE__ */ jsx11(Stat, { label: t.stats.words, value: String(totalWords) }),
|
|
2452
|
+
/* @__PURE__ */ jsx11(Stat, { label: t.stats.errors, value: String(totalErrors) }),
|
|
2453
|
+
/* @__PURE__ */ jsx11(Stat, { label: t.stats.wpm, value: String(overallWpm), accent: true }),
|
|
2454
|
+
/* @__PURE__ */ jsx11(Stat, { label: t.stats.accuracy, value: `${Math.round(overallAcc * 1e3) / 10}%`, accent: true }),
|
|
2455
|
+
/* @__PURE__ */ jsx11(Stat, { label: t.stats.streak, value: `${streak}d`, accent: true })
|
|
1965
2456
|
] })
|
|
1966
2457
|
] }),
|
|
1967
|
-
/* @__PURE__ */ jsxs6(
|
|
1968
|
-
/* @__PURE__ */
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
1974
|
-
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
1975
|
-
/* @__PURE__ */ jsx10(Text6, { color: PALETTE.muted, children: "wpm ".padEnd(10) }),
|
|
1976
|
-
/* @__PURE__ */ jsx10(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.wpm)) }),
|
|
2458
|
+
/* @__PURE__ */ jsxs6(Box7, { marginTop: 2, flexDirection: "column", children: [
|
|
2459
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.last(days) }),
|
|
2460
|
+
/* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
2461
|
+
/* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2462
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.wpm.padEnd(10) }),
|
|
2463
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.wpm)) }),
|
|
1977
2464
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
1978
2465
|
" max ",
|
|
1979
2466
|
Math.round(Math.max(...buckets.map((b) => b.wpm)))
|
|
1980
2467
|
] })
|
|
1981
2468
|
] }),
|
|
1982
|
-
/* @__PURE__ */ jsxs6(
|
|
1983
|
-
/* @__PURE__ */
|
|
1984
|
-
/* @__PURE__ */
|
|
2469
|
+
/* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2470
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.accuracy.padEnd(10) }),
|
|
2471
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.accuracy * 100)) })
|
|
1985
2472
|
] }),
|
|
1986
|
-
/* @__PURE__ */ jsxs6(
|
|
1987
|
-
/* @__PURE__ */
|
|
1988
|
-
/* @__PURE__ */
|
|
2473
|
+
/* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2474
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.sessions.padEnd(10) }),
|
|
2475
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.sessions)) })
|
|
1989
2476
|
] })
|
|
1990
2477
|
] })
|
|
1991
2478
|
] }),
|
|
1992
|
-
/* @__PURE__ */ jsxs6(
|
|
1993
|
-
/* @__PURE__ */
|
|
1994
|
-
recent.map((s, i) => /* @__PURE__ */ jsxs6(
|
|
2479
|
+
/* @__PURE__ */ jsxs6(Box7, { marginTop: 2, flexDirection: "column", children: [
|
|
2480
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.recent }),
|
|
2481
|
+
recent.map((s, i) => /* @__PURE__ */ jsxs6(Box7, { children: [
|
|
1995
2482
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
1996
2483
|
" ",
|
|
1997
2484
|
s.ts.replace("T", " ").slice(0, 16),
|
|
1998
2485
|
" "
|
|
1999
2486
|
] }),
|
|
2000
|
-
/* @__PURE__ */
|
|
2487
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.text, children: s.dictId.padEnd(14) }),
|
|
2001
2488
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2002
2489
|
" ",
|
|
2003
2490
|
"ch",
|
|
@@ -2016,37 +2503,37 @@ function StatsViewer() {
|
|
|
2016
2503
|
] })
|
|
2017
2504
|
] }, i))
|
|
2018
2505
|
] }),
|
|
2019
|
-
top.length > 0 && /* @__PURE__ */ jsxs6(
|
|
2020
|
-
/* @__PURE__ */
|
|
2021
|
-
top.map(([word, entry]) => /* @__PURE__ */ jsxs6(
|
|
2506
|
+
top.length > 0 && /* @__PURE__ */ jsxs6(Box7, { marginTop: 2, flexDirection: "column", children: [
|
|
2507
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.topMistakes }),
|
|
2508
|
+
top.map(([word, entry]) => /* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2022
2509
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.error, children: [
|
|
2023
2510
|
" ",
|
|
2024
2511
|
String(entry.count).padStart(3),
|
|
2025
2512
|
" "
|
|
2026
2513
|
] }),
|
|
2027
|
-
/* @__PURE__ */
|
|
2028
|
-
/* @__PURE__ */
|
|
2514
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.text, children: word.padEnd(20) }),
|
|
2515
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: entry.dictIds.join(", ") })
|
|
2029
2516
|
] }, word))
|
|
2030
2517
|
] }),
|
|
2031
|
-
/* @__PURE__ */
|
|
2032
|
-
/* @__PURE__ */
|
|
2518
|
+
/* @__PURE__ */ jsx11(Box7, { flexGrow: 1 }),
|
|
2519
|
+
/* @__PURE__ */ jsx11(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.footer }) })
|
|
2033
2520
|
] });
|
|
2034
2521
|
}
|
|
2035
2522
|
function Stat({ label, value, accent = false }) {
|
|
2036
|
-
return /* @__PURE__ */ jsxs6(
|
|
2523
|
+
return /* @__PURE__ */ jsxs6(Box7, { marginRight: 3, children: [
|
|
2037
2524
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2038
2525
|
label,
|
|
2039
2526
|
" "
|
|
2040
2527
|
] }),
|
|
2041
|
-
/* @__PURE__ */
|
|
2528
|
+
/* @__PURE__ */ jsx11(Text6, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
|
|
2042
2529
|
] });
|
|
2043
2530
|
}
|
|
2044
2531
|
|
|
2045
2532
|
// src/ui/screens/WordLookup.tsx
|
|
2046
|
-
import { useEffect as useEffect8, useState as
|
|
2047
|
-
import { Box as
|
|
2533
|
+
import { useEffect as useEffect8, useState as useState11 } from "react";
|
|
2534
|
+
import { Box as Box8, Text as Text7, useInput as useInput7 } from "ink";
|
|
2048
2535
|
import { readdir } from "fs/promises";
|
|
2049
|
-
import { Fragment as
|
|
2536
|
+
import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2050
2537
|
async function listLocalDictIds() {
|
|
2051
2538
|
try {
|
|
2052
2539
|
const files = await readdir(paths.dictsDir);
|
|
@@ -2057,11 +2544,12 @@ async function listLocalDictIds() {
|
|
|
2057
2544
|
}
|
|
2058
2545
|
function WordLookup() {
|
|
2059
2546
|
const nav = useNav();
|
|
2060
|
-
const
|
|
2061
|
-
const [
|
|
2062
|
-
const [
|
|
2063
|
-
const [
|
|
2064
|
-
const [
|
|
2547
|
+
const t = useStrings();
|
|
2548
|
+
const [query, setQuery] = useState11("");
|
|
2549
|
+
const [allWords, setAllWords] = useState11([]);
|
|
2550
|
+
const [book, setBook] = useState11({});
|
|
2551
|
+
const [loading, setLoading] = useState11(true);
|
|
2552
|
+
const [selected, setSelected] = useState11(0);
|
|
2065
2553
|
useEffect8(() => {
|
|
2066
2554
|
void (async () => {
|
|
2067
2555
|
const ids = await listLocalDictIds();
|
|
@@ -2102,49 +2590,45 @@ function WordLookup() {
|
|
|
2102
2590
|
}
|
|
2103
2591
|
});
|
|
2104
2592
|
if (loading) {
|
|
2105
|
-
return /* @__PURE__ */
|
|
2593
|
+
return /* @__PURE__ */ jsx12(Box8, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.indexing }) });
|
|
2106
2594
|
}
|
|
2107
2595
|
if (allWords.length === 0) {
|
|
2108
|
-
return /* @__PURE__ */ jsxs7(
|
|
2109
|
-
/* @__PURE__ */
|
|
2110
|
-
/* @__PURE__ */
|
|
2111
|
-
/* @__PURE__ */
|
|
2596
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2597
|
+
/* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.none }),
|
|
2598
|
+
/* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.pullFirst }) }),
|
|
2599
|
+
/* @__PURE__ */ jsx12(Box8, { marginTop: 2, children: /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
|
|
2600
|
+
"[Esc] ",
|
|
2601
|
+
t.common.back
|
|
2602
|
+
] }) })
|
|
2112
2603
|
] });
|
|
2113
2604
|
}
|
|
2114
2605
|
const current = filtered[selected];
|
|
2115
|
-
return /* @__PURE__ */ jsxs7(
|
|
2116
|
-
/* @__PURE__ */ jsxs7(
|
|
2117
|
-
/* @__PURE__ */
|
|
2118
|
-
/* @__PURE__ */
|
|
2119
|
-
/* @__PURE__ */
|
|
2120
|
-
allWords.length,
|
|
2121
|
-
" words across local dicts"
|
|
2122
|
-
] })
|
|
2606
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2607
|
+
/* @__PURE__ */ jsxs7(Box8, { children: [
|
|
2608
|
+
/* @__PURE__ */ jsx12(Text7, { bold: true, color: PALETTE.accent, children: t.word.title }),
|
|
2609
|
+
/* @__PURE__ */ jsx12(Box8, { flexGrow: 1 }),
|
|
2610
|
+
/* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.countAcross(allWords.length) })
|
|
2123
2611
|
] }),
|
|
2124
|
-
/* @__PURE__ */ jsxs7(
|
|
2125
|
-
/* @__PURE__ */
|
|
2126
|
-
/* @__PURE__ */
|
|
2127
|
-
/* @__PURE__ */
|
|
2612
|
+
/* @__PURE__ */ jsxs7(Box8, { marginTop: 1, children: [
|
|
2613
|
+
/* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: "> " }),
|
|
2614
|
+
/* @__PURE__ */ jsx12(Text7, { color: PALETTE.text, children: query }),
|
|
2615
|
+
/* @__PURE__ */ jsx12(Text7, { color: PALETTE.accent, children: "_" })
|
|
2128
2616
|
] }),
|
|
2129
|
-
/* @__PURE__ */ jsxs7(
|
|
2130
|
-
/* @__PURE__ */ jsxs7(
|
|
2617
|
+
/* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexGrow: 1, children: [
|
|
2618
|
+
/* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", width: "40%", children: [
|
|
2131
2619
|
filtered.map((h, i) => {
|
|
2132
2620
|
const active = i === selected;
|
|
2133
|
-
return /* @__PURE__ */ jsxs7(
|
|
2134
|
-
/* @__PURE__ */
|
|
2135
|
-
/* @__PURE__ */
|
|
2136
|
-
/* @__PURE__ */
|
|
2621
|
+
return /* @__PURE__ */ jsxs7(Box8, { children: [
|
|
2622
|
+
/* @__PURE__ */ jsx12(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2623
|
+
/* @__PURE__ */ jsx12(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: h.word.name.padEnd(20) }),
|
|
2624
|
+
/* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: h.dictId })
|
|
2137
2625
|
] }, `${h.dictId}-${h.word.name}-${i}`);
|
|
2138
2626
|
}),
|
|
2139
|
-
filtered.length === 0 && q && /* @__PURE__ */
|
|
2140
|
-
'no matches for "',
|
|
2141
|
-
query,
|
|
2142
|
-
'"'
|
|
2143
|
-
] })
|
|
2627
|
+
filtered.length === 0 && q && /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.noMatches(query) })
|
|
2144
2628
|
] }),
|
|
2145
|
-
/* @__PURE__ */
|
|
2146
|
-
/* @__PURE__ */
|
|
2147
|
-
/* @__PURE__ */ jsxs7(
|
|
2629
|
+
/* @__PURE__ */ jsx12(Box8, { flexDirection: "column", width: "60%", paddingLeft: 2, children: current && /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2630
|
+
/* @__PURE__ */ jsx12(Text7, { bold: true, color: PALETTE.text, children: current.word.name }),
|
|
2631
|
+
/* @__PURE__ */ jsxs7(Box8, { marginTop: 1, children: [
|
|
2148
2632
|
current.word.usphone && /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
|
|
2149
2633
|
"US /",
|
|
2150
2634
|
current.word.usphone,
|
|
@@ -2156,59 +2640,109 @@ function WordLookup() {
|
|
|
2156
2640
|
"/"
|
|
2157
2641
|
] })
|
|
2158
2642
|
] }),
|
|
2159
|
-
/* @__PURE__ */
|
|
2643
|
+
/* @__PURE__ */ jsx12(Box8, { marginTop: 1, flexDirection: "column", children: (current.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.primary, children: [
|
|
2160
2644
|
"\xB7 ",
|
|
2161
|
-
|
|
2645
|
+
tr
|
|
2162
2646
|
] }, i)) }),
|
|
2163
|
-
/* @__PURE__ */
|
|
2164
|
-
|
|
2165
|
-
current.dictId
|
|
2166
|
-
] }) }),
|
|
2167
|
-
book[current.word.name] && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
2168
|
-
/* @__PURE__ */ jsxs7(Text7, { color: PALETTE.error, children: [
|
|
2169
|
-
"mistakes: ",
|
|
2170
|
-
book[current.word.name].count
|
|
2171
|
-
] }),
|
|
2172
|
-
/* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
|
|
2173
|
-
" ",
|
|
2174
|
-
"(last ",
|
|
2175
|
-
book[current.word.name].lastSeen.slice(0, 10),
|
|
2176
|
-
")"
|
|
2177
|
-
] })
|
|
2178
|
-
] })
|
|
2647
|
+
/* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.inDict(current.dictId) }) }),
|
|
2648
|
+
book[current.word.name] && /* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.error, children: t.word.mistakes(book[current.word.name].count, book[current.word.name].lastSeen.slice(0, 10)) }) })
|
|
2179
2649
|
] }) })
|
|
2180
2650
|
] }),
|
|
2181
|
-
/* @__PURE__ */
|
|
2651
|
+
/* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.footer }) })
|
|
2182
2652
|
] });
|
|
2183
2653
|
}
|
|
2184
2654
|
|
|
2185
2655
|
// src/ui/App.tsx
|
|
2186
|
-
import { jsx as
|
|
2656
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
2187
2657
|
function App({ initial, initialCfg }) {
|
|
2188
|
-
return /* @__PURE__ */
|
|
2658
|
+
return /* @__PURE__ */ jsx13(AppStateProvider, { initialCfg, children: /* @__PURE__ */ jsx13(LangBridge, { children: /* @__PURE__ */ jsx13(AudioStatusProvider, { disabled: !initialCfg.sounds.master, children: /* @__PURE__ */ jsx13(NavProvider, { initial, children: /* @__PURE__ */ jsx13(Fullscreen, { children: /* @__PURE__ */ jsx13(Router, {}) }) }) }) }) });
|
|
2659
|
+
}
|
|
2660
|
+
function LangBridge({ children }) {
|
|
2661
|
+
const { cfg } = useAppState();
|
|
2662
|
+
return /* @__PURE__ */ jsx13(StringsProvider, { pref: cfg.language, children });
|
|
2663
|
+
}
|
|
2664
|
+
function screenKey(frame) {
|
|
2665
|
+
if (frame.name === "practice") {
|
|
2666
|
+
const p = frame.params;
|
|
2667
|
+
return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}`;
|
|
2668
|
+
}
|
|
2669
|
+
return frame.name;
|
|
2189
2670
|
}
|
|
2190
2671
|
function Router() {
|
|
2191
2672
|
const nav = useNav();
|
|
2192
2673
|
const { cfg } = useAppState();
|
|
2193
2674
|
const { exit } = useApp4();
|
|
2194
|
-
|
|
2195
|
-
|
|
2675
|
+
const lastKeyRef = useRef4(null);
|
|
2676
|
+
useInput8((input, key2) => {
|
|
2677
|
+
if (key2.ctrl && input === "c") exit();
|
|
2196
2678
|
});
|
|
2197
2679
|
const frame = nav.current;
|
|
2680
|
+
const key = screenKey(frame);
|
|
2681
|
+
if (lastKeyRef.current !== key) {
|
|
2682
|
+
if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[H");
|
|
2683
|
+
lastKeyRef.current = key;
|
|
2684
|
+
}
|
|
2198
2685
|
switch (frame.name) {
|
|
2199
2686
|
case "main":
|
|
2200
|
-
return /* @__PURE__ */
|
|
2687
|
+
return /* @__PURE__ */ jsx13(MainMenu, { cfg });
|
|
2201
2688
|
case "practice":
|
|
2202
|
-
return /* @__PURE__ */
|
|
2689
|
+
return /* @__PURE__ */ jsx13(PracticeScreen, { params: frame.params });
|
|
2203
2690
|
case "dict":
|
|
2204
|
-
return /* @__PURE__ */
|
|
2691
|
+
return /* @__PURE__ */ jsx13(DictBrowser, { params: frame.params });
|
|
2205
2692
|
case "config":
|
|
2206
|
-
return /* @__PURE__ */
|
|
2693
|
+
return /* @__PURE__ */ jsx13(ConfigEditor, {});
|
|
2207
2694
|
case "stats":
|
|
2208
|
-
return /* @__PURE__ */
|
|
2695
|
+
return /* @__PURE__ */ jsx13(StatsViewer, {});
|
|
2209
2696
|
case "word":
|
|
2210
|
-
return /* @__PURE__ */
|
|
2697
|
+
return /* @__PURE__ */ jsx13(WordLookup, {});
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/util/report.ts
|
|
2702
|
+
import chalk3 from "chalk";
|
|
2703
|
+
var LEAVE_ALTSCREEN = "\x1B[?25h\x1B[?1049l";
|
|
2704
|
+
function ensureMainScreen() {
|
|
2705
|
+
if (process.stdout.isTTY) process.stdout.write(LEAVE_ALTSCREEN);
|
|
2706
|
+
}
|
|
2707
|
+
function fmtDuration(ms, lang) {
|
|
2708
|
+
const total = Math.floor(ms / 1e3);
|
|
2709
|
+
const m = Math.floor(total / 60);
|
|
2710
|
+
const s = total % 60;
|
|
2711
|
+
if (lang === "zh") {
|
|
2712
|
+
if (m === 0) return `${s} \u79D2`;
|
|
2713
|
+
return `${m} \u5206 ${s} \u79D2`;
|
|
2714
|
+
}
|
|
2715
|
+
if (m === 0) return `${s}s`;
|
|
2716
|
+
return `${m}m ${s}s`;
|
|
2717
|
+
}
|
|
2718
|
+
function printSessionReport(r, t, lang) {
|
|
2719
|
+
if (r.chaptersCompleted === 0) return;
|
|
2720
|
+
const accPct = Math.round(r.accuracy * 1e3) / 10;
|
|
2721
|
+
const labels = [
|
|
2722
|
+
t.report.duration,
|
|
2723
|
+
t.report.practiced,
|
|
2724
|
+
t.report.chapters,
|
|
2725
|
+
t.report.words,
|
|
2726
|
+
t.report.accuracy,
|
|
2727
|
+
t.report.wpm
|
|
2728
|
+
];
|
|
2729
|
+
if (r.newMistakeWords > 0) labels.push(t.report.newMistakes);
|
|
2730
|
+
const labelW = Math.max(...labels.map(visibleWidth2)) + 2;
|
|
2731
|
+
const pad = (label) => label + " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
|
|
2732
|
+
console.log();
|
|
2733
|
+
console.log(chalk3.bold.cyan(t.report.title));
|
|
2734
|
+
console.log(` ${chalk3.dim(pad(t.report.duration))} ${fmtDuration(r.totalDurationMs, lang)}`);
|
|
2735
|
+
console.log(` ${chalk3.dim(pad(t.report.practiced))} ${fmtDuration(r.practiceMs, lang)}`);
|
|
2736
|
+
console.log(` ${chalk3.dim(pad(t.report.chapters))} ${r.chaptersCompleted}`);
|
|
2737
|
+
console.log(` ${chalk3.dim(pad(t.report.words))} ${r.wordCount}`);
|
|
2738
|
+
console.log(` ${chalk3.dim(pad(t.report.accuracy))} ${accPct}%`);
|
|
2739
|
+
console.log(` ${chalk3.dim(pad(t.report.wpm))} ${r.wpm}`);
|
|
2740
|
+
if (r.newMistakeWords > 0) {
|
|
2741
|
+
console.log(` ${chalk3.dim(pad(t.report.newMistakes))} ${r.newMistakeWords}`);
|
|
2211
2742
|
}
|
|
2743
|
+
console.log();
|
|
2744
|
+
console.log(chalk3.dim(` ${t.report.farewell}`));
|
|
2745
|
+
console.log();
|
|
2212
2746
|
}
|
|
2213
2747
|
|
|
2214
2748
|
// src/commands/practice.ts
|
|
@@ -2218,24 +2752,25 @@ function isMode(v) {
|
|
|
2218
2752
|
}
|
|
2219
2753
|
async function runPractice(dictIdArg, options) {
|
|
2220
2754
|
if (!process.stdout.isTTY) {
|
|
2221
|
-
console.error(
|
|
2755
|
+
console.error(chalk4.red("Practice requires an interactive TTY."));
|
|
2222
2756
|
process.exitCode = 1;
|
|
2223
2757
|
return;
|
|
2224
2758
|
}
|
|
2225
2759
|
const cfg = await loadConfig();
|
|
2226
2760
|
const dictId = dictIdArg ?? cfg.defaultDict;
|
|
2227
2761
|
if (!dictId) {
|
|
2228
|
-
console.error(
|
|
2762
|
+
console.error(chalk4.red("No dictionary specified. Pass an id or set config.defaultDict."));
|
|
2229
2763
|
process.exitCode = 1;
|
|
2230
2764
|
return;
|
|
2231
2765
|
}
|
|
2232
2766
|
const mode = options.mode ?? cfg.defaultMode;
|
|
2233
2767
|
if (!isMode(mode)) {
|
|
2234
|
-
console.error(
|
|
2768
|
+
console.error(chalk4.red(`Invalid mode "${mode}". Valid: ${MODES.join(", ")}`));
|
|
2235
2769
|
process.exitCode = 1;
|
|
2236
2770
|
return;
|
|
2237
2771
|
}
|
|
2238
2772
|
const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);
|
|
2773
|
+
start();
|
|
2239
2774
|
const { waitUntilExit } = render(
|
|
2240
2775
|
createElement(App, {
|
|
2241
2776
|
initial: { name: "practice", params: { dictId, chapterIndex, mode } },
|
|
@@ -2244,6 +2779,9 @@ async function runPractice(dictIdArg, options) {
|
|
|
2244
2779
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
2245
2780
|
);
|
|
2246
2781
|
await waitUntilExit();
|
|
2782
|
+
ensureMainScreen();
|
|
2783
|
+
const { lang, t } = pickStrings(cfg.language);
|
|
2784
|
+
printSessionReport(report(), t, lang);
|
|
2247
2785
|
}
|
|
2248
2786
|
function buildPracticeCommand() {
|
|
2249
2787
|
return new Command3("practice").argument("[dictId]", "dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>", "chapter number (1-based)", "1").option("-m, --mode <mode>", "order | dictation | review | random | loop").action(async (dictIdArg, options) => {
|
|
@@ -2253,7 +2791,7 @@ function buildPracticeCommand() {
|
|
|
2253
2791
|
|
|
2254
2792
|
// src/commands/stats.ts
|
|
2255
2793
|
import { Command as Command4 } from "commander";
|
|
2256
|
-
import
|
|
2794
|
+
import chalk5 from "chalk";
|
|
2257
2795
|
function buildStatsCommand() {
|
|
2258
2796
|
return new Command4("stats").description("Show practice history and trends").option("-d, --days <n>", "window size for trend (default 14)", "14").option("--top <n>", "how many top mistakes to show (default 10)", "10").action(async (opts) => {
|
|
2259
2797
|
const days = Math.max(1, Number(opts.days) || 14);
|
|
@@ -2261,7 +2799,7 @@ function buildStatsCommand() {
|
|
|
2261
2799
|
const sessions = await loadSessions();
|
|
2262
2800
|
const book = await loadMistakes();
|
|
2263
2801
|
if (sessions.length === 0) {
|
|
2264
|
-
console.log(
|
|
2802
|
+
console.log(chalk5.yellow("No practice history yet. Run `qwerty practice <dict>` to get started."));
|
|
2265
2803
|
return;
|
|
2266
2804
|
}
|
|
2267
2805
|
const buckets = dailyBuckets(sessions, days);
|
|
@@ -2275,33 +2813,33 @@ function buildStatsCommand() {
|
|
|
2275
2813
|
);
|
|
2276
2814
|
const overallWpm = totalMs > 0 ? Math.round(totalWords / (totalMs / 6e4) * 10) / 10 : 0;
|
|
2277
2815
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
2278
|
-
console.log(
|
|
2279
|
-
console.log(` ${
|
|
2280
|
-
console.log(` ${
|
|
2281
|
-
console.log(
|
|
2816
|
+
console.log(chalk5.bold("\nLifetime"));
|
|
2817
|
+
console.log(` ${chalk5.dim("sessions")} ${sessions.length} ${chalk5.dim("words")} ${totalWords} ${chalk5.dim("errors")} ${totalErrors}`);
|
|
2818
|
+
console.log(` ${chalk5.dim("avg wpm")} ${overallWpm} ${chalk5.dim("avg accuracy")} ${Math.round(overallAcc * 1e3) / 10}% ${chalk5.dim("streak")} ${chalk5.bold(streak)}d`);
|
|
2819
|
+
console.log(chalk5.bold(`
|
|
2282
2820
|
Last ${days} days`));
|
|
2283
|
-
console.log(` ${
|
|
2284
|
-
console.log(` ${
|
|
2285
|
-
console.log(` ${
|
|
2821
|
+
console.log(` ${chalk5.dim("wpm ")} ${sparkline(buckets.map((b) => b.wpm))} ${chalk5.dim("max")} ${Math.round(Math.max(...buckets.map((b) => b.wpm)))}`);
|
|
2822
|
+
console.log(` ${chalk5.dim("accuracy")} ${sparkline(buckets.map((b) => b.accuracy * 100))} ${chalk5.dim("range")} ${Math.round(Math.min(...buckets.map((b) => b.accuracy * 100)))}-${Math.round(Math.max(...buckets.map((b) => b.accuracy * 100)))}%`);
|
|
2823
|
+
console.log(` ${chalk5.dim("sessions")} ${sparkline(buckets.map((b) => b.sessions))}`);
|
|
2286
2824
|
const recent = sessions.slice(-5).reverse();
|
|
2287
|
-
console.log(
|
|
2825
|
+
console.log(chalk5.bold("\nLast 5 sessions"));
|
|
2288
2826
|
for (const s of recent) {
|
|
2289
2827
|
const wpm = computeWPM(s);
|
|
2290
2828
|
const acc = Math.round(accuracy(s) * 1e3) / 10;
|
|
2291
2829
|
console.log(
|
|
2292
|
-
` ${
|
|
2830
|
+
` ${chalk5.dim(s.ts.replace("T", " ").slice(0, 16))} ${chalk5.cyan(s.dictId.padEnd(14))} ch${String(s.chapter + 1).padStart(3)} ${s.mode.padEnd(9)} ${String(s.wordCount).padStart(3)}w ${s.errors}err ${wpm}wpm ${acc}%`
|
|
2293
2831
|
);
|
|
2294
2832
|
}
|
|
2295
2833
|
const top = topN(book, topCount);
|
|
2296
2834
|
if (top.length > 0) {
|
|
2297
|
-
console.log(
|
|
2835
|
+
console.log(chalk5.bold(`
|
|
2298
2836
|
Top ${top.length} mistakes`));
|
|
2299
2837
|
for (const [word, entry] of top) {
|
|
2300
|
-
console.log(` ${
|
|
2838
|
+
console.log(` ${chalk5.red(String(entry.count).padStart(3))} ${chalk5.bold(word.padEnd(20))} ${chalk5.dim(entry.dictIds.join(", "))}`);
|
|
2301
2839
|
}
|
|
2302
2840
|
} else {
|
|
2303
|
-
console.log(
|
|
2304
|
-
console.log(
|
|
2841
|
+
console.log(chalk5.bold("\nTop mistakes"));
|
|
2842
|
+
console.log(chalk5.dim(" none \u2014 keep going"));
|
|
2305
2843
|
}
|
|
2306
2844
|
console.log();
|
|
2307
2845
|
});
|
|
@@ -2309,7 +2847,7 @@ Top ${top.length} mistakes`));
|
|
|
2309
2847
|
|
|
2310
2848
|
// src/commands/word.ts
|
|
2311
2849
|
import { Command as Command5 } from "commander";
|
|
2312
|
-
import
|
|
2850
|
+
import chalk6 from "chalk";
|
|
2313
2851
|
import { readdir as readdir2 } from "fs/promises";
|
|
2314
2852
|
async function listLocalDictIds2() {
|
|
2315
2853
|
try {
|
|
@@ -2324,7 +2862,7 @@ function buildWordCommand() {
|
|
|
2324
2862
|
const q = keyword.toLowerCase();
|
|
2325
2863
|
const ids = await listLocalDictIds2();
|
|
2326
2864
|
if (ids.length === 0) {
|
|
2327
|
-
console.log(
|
|
2865
|
+
console.log(chalk6.yellow("No local dictionaries. Run `qwerty dict pull <id>` first."));
|
|
2328
2866
|
return;
|
|
2329
2867
|
}
|
|
2330
2868
|
const hits = [];
|
|
@@ -2338,7 +2876,7 @@ function buildWordCommand() {
|
|
|
2338
2876
|
}
|
|
2339
2877
|
}
|
|
2340
2878
|
if (hits.length === 0) {
|
|
2341
|
-
console.log(
|
|
2879
|
+
console.log(chalk6.yellow(`No matches for "${keyword}" in ${ids.length} local dictionaries`));
|
|
2342
2880
|
return;
|
|
2343
2881
|
}
|
|
2344
2882
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2351,21 +2889,21 @@ function buildWordCommand() {
|
|
|
2351
2889
|
for (const [name, group] of byName) {
|
|
2352
2890
|
const first = group[0].word;
|
|
2353
2891
|
console.log();
|
|
2354
|
-
console.log(
|
|
2892
|
+
console.log(chalk6.bold.white(name));
|
|
2355
2893
|
const us = first.usphone ? `US /${first.usphone}/` : "";
|
|
2356
2894
|
const uk = first.ukphone ? `UK /${first.ukphone}/` : "";
|
|
2357
|
-
if (us || uk) console.log(
|
|
2358
|
-
for (const t of first.trans ?? []) console.log(
|
|
2895
|
+
if (us || uk) console.log(chalk6.dim(` ${[us, uk].filter(Boolean).join(" ")}`));
|
|
2896
|
+
for (const t of first.trans ?? []) console.log(chalk6.cyan(` \xB7 ${t}`));
|
|
2359
2897
|
const sources = await Promise.all(
|
|
2360
2898
|
group.map(async (h) => {
|
|
2361
2899
|
const reg = await findEntry(h.dictId);
|
|
2362
2900
|
return reg?.name ?? h.dictId;
|
|
2363
2901
|
})
|
|
2364
2902
|
);
|
|
2365
|
-
console.log(
|
|
2903
|
+
console.log(chalk6.dim(` in: ${sources.join(", ")}`));
|
|
2366
2904
|
const mistake = book[name];
|
|
2367
2905
|
if (mistake) {
|
|
2368
|
-
console.log(
|
|
2906
|
+
console.log(chalk6.dim(` mistakes: ${mistake.count} (last ${mistake.lastSeen.slice(0, 10)})`));
|
|
2369
2907
|
}
|
|
2370
2908
|
}
|
|
2371
2909
|
console.log();
|
|
@@ -2381,16 +2919,20 @@ async function runMainMenu() {
|
|
|
2381
2919
|
return;
|
|
2382
2920
|
}
|
|
2383
2921
|
const cfg = await loadConfig();
|
|
2922
|
+
start();
|
|
2384
2923
|
const { waitUntilExit } = render2(
|
|
2385
2924
|
createElement2(App, { initial: { name: "main" }, initialCfg: cfg }),
|
|
2386
2925
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
2387
2926
|
);
|
|
2388
2927
|
await waitUntilExit();
|
|
2928
|
+
ensureMainScreen();
|
|
2929
|
+
const { lang, t } = pickStrings(cfg.language);
|
|
2930
|
+
printSessionReport(report(), t, lang);
|
|
2389
2931
|
}
|
|
2390
2932
|
|
|
2391
2933
|
// src/cli.ts
|
|
2392
2934
|
var program = new Command6();
|
|
2393
|
-
program.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version(
|
|
2935
|
+
program.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version(package_default.version);
|
|
2394
2936
|
program.addCommand(buildPracticeCommand());
|
|
2395
2937
|
program.addCommand(buildDictCommand());
|
|
2396
2938
|
program.addCommand(buildWordCommand());
|