qwerty-cli 0.0.1-alpha.5 → 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 +879 -398
- package/dist/cli.js.map +1 -1
- package/package.json +1 -2
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Command as Command6 } from "commander";
|
|
|
4
4
|
// package.json
|
|
5
5
|
var package_default = {
|
|
6
6
|
name: "qwerty-cli",
|
|
7
|
-
version: "0.0.1-alpha.
|
|
7
|
+
version: "0.0.1-alpha.6",
|
|
8
8
|
description: "Terminal clone of qwerty-learner: typing practice for English vocabulary, with chapters, dictation, mistake book, and audio.",
|
|
9
9
|
type: "module",
|
|
10
10
|
bin: {
|
|
@@ -40,7 +40,6 @@ var package_default = {
|
|
|
40
40
|
chalk: "^5.3.0",
|
|
41
41
|
commander: "^12.1.0",
|
|
42
42
|
ink: "^5.0.1",
|
|
43
|
-
"ink-big-text": "^2.0.0",
|
|
44
43
|
"p-queue": "^8.0.1",
|
|
45
44
|
react: "^18.3.1",
|
|
46
45
|
undici: "^6.19.8",
|
|
@@ -150,7 +149,8 @@ var ConfigSchema = z.object({
|
|
|
150
149
|
}).default({ master: true, keystroke: true, feedback: true, keySoundName: "default" }),
|
|
151
150
|
autoplayPronunciation: z.boolean().default(true),
|
|
152
151
|
defaultMode: z.enum(["order", "dictation", "review", "random", "loop"]).default("order"),
|
|
153
|
-
defaultDict: z.string().optional()
|
|
152
|
+
defaultDict: z.string().optional(),
|
|
153
|
+
language: z.enum(["auto", "zh", "en"]).default("auto")
|
|
154
154
|
});
|
|
155
155
|
var DEFAULTS = ConfigSchema.parse({});
|
|
156
156
|
async function loadConfig() {
|
|
@@ -506,11 +506,12 @@ ${matches.length} matches`));
|
|
|
506
506
|
|
|
507
507
|
// src/commands/practice.ts
|
|
508
508
|
import { Command as Command3 } from "commander";
|
|
509
|
-
import
|
|
509
|
+
import chalk4 from "chalk";
|
|
510
510
|
import { render } from "ink";
|
|
511
511
|
import { createElement } from "react";
|
|
512
512
|
|
|
513
513
|
// src/ui/App.tsx
|
|
514
|
+
import { useRef as useRef4 } from "react";
|
|
514
515
|
import { useApp as useApp4, useInput as useInput8 } from "ink";
|
|
515
516
|
|
|
516
517
|
// src/ui/nav.tsx
|
|
@@ -541,17 +542,27 @@ function useNav() {
|
|
|
541
542
|
}
|
|
542
543
|
|
|
543
544
|
// src/ui/Fullscreen.tsx
|
|
544
|
-
import { useEffect } from "react";
|
|
545
|
-
import {
|
|
546
|
-
|
|
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";
|
|
547
549
|
var LEAVE = "\x1B[?25h\x1B[?1049l";
|
|
548
550
|
function shouldUse() {
|
|
549
551
|
return Boolean(process.stdout.isTTY) && process.env.QWERTY_NO_ALTSCREEN !== "1";
|
|
550
552
|
}
|
|
551
553
|
function Fullscreen({ children }) {
|
|
554
|
+
const { stdout } = useStdout();
|
|
555
|
+
const [size, setSize] = useState2(() => ({
|
|
556
|
+
rows: stdout?.rows ?? 24,
|
|
557
|
+
cols: stdout?.columns ?? 80
|
|
558
|
+
}));
|
|
552
559
|
useEffect(() => {
|
|
553
560
|
if (!shouldUse()) return;
|
|
554
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);
|
|
555
566
|
const leave = () => {
|
|
556
567
|
try {
|
|
557
568
|
process.stdout.write(LEAVE);
|
|
@@ -569,14 +580,15 @@ function Fullscreen({ children }) {
|
|
|
569
580
|
process.off("SIGINT", onSignal);
|
|
570
581
|
process.off("SIGTERM", onSignal);
|
|
571
582
|
process.off("exit", leave);
|
|
583
|
+
process.stdout.off("resize", onResize);
|
|
572
584
|
leave();
|
|
573
585
|
};
|
|
574
586
|
}, []);
|
|
575
|
-
return /* @__PURE__ */ jsx2(
|
|
587
|
+
return /* @__PURE__ */ jsx2(Box, { width: size.cols, height: size.rows, flexDirection: "column", children });
|
|
576
588
|
}
|
|
577
589
|
|
|
578
590
|
// src/ui/audio-context.tsx
|
|
579
|
-
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";
|
|
580
592
|
|
|
581
593
|
// src/infra/audio.ts
|
|
582
594
|
import { spawn } from "child_process";
|
|
@@ -738,7 +750,7 @@ function AudioStatusProvider({
|
|
|
738
750
|
disabled,
|
|
739
751
|
children
|
|
740
752
|
}) {
|
|
741
|
-
const [status, setStatus] =
|
|
753
|
+
const [status, setStatus] = useState3({ warning: null, ready: false });
|
|
742
754
|
useEffect2(() => {
|
|
743
755
|
let cancelled = false;
|
|
744
756
|
initAudio(disabled).then(() => {
|
|
@@ -759,14 +771,14 @@ function useAudioStatus() {
|
|
|
759
771
|
}
|
|
760
772
|
|
|
761
773
|
// src/ui/app-state.tsx
|
|
762
|
-
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";
|
|
763
775
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
764
776
|
var AppStateContext = createContext3(null);
|
|
765
777
|
function AppStateProvider({
|
|
766
778
|
initialCfg,
|
|
767
779
|
children
|
|
768
780
|
}) {
|
|
769
|
-
const [cfg, setCfgState] =
|
|
781
|
+
const [cfg, setCfgState] = useState4(initialCfg);
|
|
770
782
|
const setCfg = useCallback2(async (next) => {
|
|
771
783
|
setCfgState(next);
|
|
772
784
|
await saveConfig(next);
|
|
@@ -779,14 +791,367 @@ function useAppState() {
|
|
|
779
791
|
return ctx;
|
|
780
792
|
}
|
|
781
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
|
+
|
|
782
1148
|
// src/ui/screens/MainMenu.tsx
|
|
783
|
-
import { useState as
|
|
784
|
-
import { Box as
|
|
1149
|
+
import { useState as useState5 } from "react";
|
|
1150
|
+
import { Box as Box3, Text as Text2, useApp, useInput } from "ink";
|
|
785
1151
|
|
|
786
1152
|
// src/ui/components/BigWord.tsx
|
|
787
|
-
import { Box, Text, useStdout } from "ink";
|
|
788
|
-
import
|
|
789
|
-
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";
|
|
790
1155
|
var PALETTE = {
|
|
791
1156
|
accent: "#5eead4",
|
|
792
1157
|
muted: "#6b7280",
|
|
@@ -797,42 +1162,58 @@ var PALETTE = {
|
|
|
797
1162
|
error: "#f87171"
|
|
798
1163
|
};
|
|
799
1164
|
function BigWord({ target, typed, error = false, hideTarget = false }) {
|
|
800
|
-
const { stdout } =
|
|
1165
|
+
const { stdout } = useStdout2();
|
|
801
1166
|
const cols = stdout?.columns ?? 80;
|
|
802
1167
|
const chars = [...target];
|
|
803
1168
|
const typedChars = [...typed];
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
return /* @__PURE__ */ jsx5(Box, { justifyContent: "center", children: chars.map((ch, i) => {
|
|
807
|
-
const isTyped = i < typedChars.length;
|
|
808
|
-
const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
|
|
809
|
-
const color = isTyped ? PALETTE.accent : error ? PALETTE.error : PALETTE.muted;
|
|
810
|
-
return /* @__PURE__ */ jsxs(Text, { bold: isTyped, color, underline: !isTyped && error, children: [
|
|
811
|
-
display,
|
|
812
|
-
" "
|
|
813
|
-
] }, i);
|
|
814
|
-
}) });
|
|
815
|
-
}
|
|
816
|
-
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) => {
|
|
817
1171
|
const isTyped = i < typedChars.length;
|
|
818
1172
|
const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
|
|
819
1173
|
const color = isTyped ? PALETTE.accent : error ? PALETTE.error : PALETTE.muted;
|
|
820
|
-
return /* @__PURE__ */
|
|
1174
|
+
return /* @__PURE__ */ jsxs(Text, { bold: true, color, underline: !isTyped && error, children: [
|
|
1175
|
+
display,
|
|
1176
|
+
i < chars.length - 1 ? sep : ""
|
|
1177
|
+
] }, i);
|
|
821
1178
|
}) });
|
|
822
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
|
+
}
|
|
823
1202
|
|
|
824
1203
|
// src/ui/screens/MainMenu.tsx
|
|
825
|
-
import { jsx as
|
|
1204
|
+
import { jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
826
1205
|
function MainMenu({ cfg }) {
|
|
827
|
-
const [selected, setSelected] =
|
|
1206
|
+
const [selected, setSelected] = useState5(0);
|
|
828
1207
|
const { exit } = useApp();
|
|
829
1208
|
const nav = useNav();
|
|
830
1209
|
const audio = useAudioStatus();
|
|
1210
|
+
const t = useStrings();
|
|
1211
|
+
const m = t.mainMenu.items;
|
|
831
1212
|
const items = [
|
|
832
1213
|
{
|
|
833
1214
|
key: "p",
|
|
834
|
-
label:
|
|
835
|
-
hint: cfg.defaultDict ?
|
|
1215
|
+
label: m.practiceLabel,
|
|
1216
|
+
hint: cfg.defaultDict ? m.practiceHintWith(cfg.defaultDict) : m.practiceHintNone,
|
|
836
1217
|
run: () => {
|
|
837
1218
|
if (cfg.defaultDict) {
|
|
838
1219
|
nav.navigate({
|
|
@@ -844,12 +1225,13 @@ function MainMenu({ cfg }) {
|
|
|
844
1225
|
}
|
|
845
1226
|
}
|
|
846
1227
|
},
|
|
847
|
-
{ key: "d", label:
|
|
848
|
-
{ key: "w", label:
|
|
849
|
-
{ key: "s", label:
|
|
850
|
-
{ key: "c", label:
|
|
851
|
-
{ 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() }
|
|
852
1233
|
];
|
|
1234
|
+
const labelW = Math.max(...items.map((it) => visibleWidth2(it.label))) + 4;
|
|
853
1235
|
useInput((input, key) => {
|
|
854
1236
|
if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);
|
|
855
1237
|
if (key.downArrow) setSelected((i) => (i + 1) % items.length);
|
|
@@ -864,41 +1246,41 @@ function MainMenu({ cfg }) {
|
|
|
864
1246
|
}
|
|
865
1247
|
}
|
|
866
1248
|
});
|
|
867
|
-
return /* @__PURE__ */ jsxs2(
|
|
868
|
-
/* @__PURE__ */ jsxs2(
|
|
869
|
-
/* @__PURE__ */
|
|
870
|
-
/* @__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
|
+
] })
|
|
871
1256
|
] }),
|
|
872
|
-
/* @__PURE__ */
|
|
1257
|
+
/* @__PURE__ */ jsx7(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
|
|
873
1258
|
const active = i === selected;
|
|
874
|
-
|
|
875
|
-
|
|
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 " : " " }),
|
|
876
1262
|
/* @__PURE__ */ jsxs2(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
|
|
877
1263
|
"[",
|
|
878
1264
|
it.key,
|
|
879
1265
|
"]"
|
|
880
1266
|
] }),
|
|
881
|
-
/* @__PURE__ */
|
|
882
|
-
/* @__PURE__ */
|
|
883
|
-
|
|
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 })
|
|
884
1273
|
] }, it.key);
|
|
885
1274
|
}) }),
|
|
886
|
-
/* @__PURE__ */
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
] }) }),
|
|
890
|
-
/* @__PURE__ */ jsx6(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text2, { color: PALETTE.muted, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump" }) }),
|
|
891
|
-
audio.warning && /* @__PURE__ */ jsx6(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.warning, children: [
|
|
892
|
-
"! ",
|
|
893
|
-
audio.warning
|
|
894
|
-
] }) })
|
|
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 }) })
|
|
895
1278
|
] });
|
|
896
1279
|
}
|
|
897
1280
|
|
|
898
1281
|
// src/ui/screens/PracticeScreen.tsx
|
|
899
|
-
import { useState as
|
|
900
|
-
import { Box as
|
|
901
|
-
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";
|
|
902
1284
|
|
|
903
1285
|
// src/util/shuffle.ts
|
|
904
1286
|
function shuffle(arr, rng = Math.random) {
|
|
@@ -942,25 +1324,25 @@ function buildPlaylist(chapter, mode, seed) {
|
|
|
942
1324
|
function initialState(target) {
|
|
943
1325
|
return { target, typed: "", errorsThisWord: 0 };
|
|
944
1326
|
}
|
|
945
|
-
function reduce(
|
|
1327
|
+
function reduce(state2, ev) {
|
|
946
1328
|
switch (ev.type) {
|
|
947
1329
|
case "reset":
|
|
948
|
-
return { state: { ...
|
|
1330
|
+
return { state: { ...state2, typed: "" }, effect: "none" };
|
|
949
1331
|
case "backspace": {
|
|
950
|
-
if (
|
|
951
|
-
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" };
|
|
952
1334
|
}
|
|
953
1335
|
case "char": {
|
|
954
|
-
const candidate =
|
|
955
|
-
const targetUpToCandidate = [...
|
|
1336
|
+
const candidate = state2.typed + ev.ch;
|
|
1337
|
+
const targetUpToCandidate = [...state2.target].slice(0, [...candidate].length).join("");
|
|
956
1338
|
if (candidate === targetUpToCandidate) {
|
|
957
|
-
if (candidate.length ===
|
|
958
|
-
return { state: { ...
|
|
1339
|
+
if (candidate.length === state2.target.length) {
|
|
1340
|
+
return { state: { ...state2, typed: candidate }, effect: "correct" };
|
|
959
1341
|
}
|
|
960
|
-
return { state: { ...
|
|
1342
|
+
return { state: { ...state2, typed: candidate }, effect: "progress" };
|
|
961
1343
|
}
|
|
962
1344
|
return {
|
|
963
|
-
state: { ...
|
|
1345
|
+
state: { ...state2, typed: "", errorsThisWord: state2.errorsThisWord + 1 },
|
|
964
1346
|
effect: "wrong"
|
|
965
1347
|
};
|
|
966
1348
|
}
|
|
@@ -982,11 +1364,11 @@ function startSession(playlist, now = Date.now()) {
|
|
|
982
1364
|
}
|
|
983
1365
|
function feedSession(session, ev, now = Date.now()) {
|
|
984
1366
|
if (!session.current) return { session, effect: "none" };
|
|
985
|
-
const { state, effect } = reduce(session.current.input, ev);
|
|
1367
|
+
const { state: state2, effect } = reduce(session.current.input, ev);
|
|
986
1368
|
if (effect === "correct") {
|
|
987
1369
|
const finished = {
|
|
988
|
-
word:
|
|
989
|
-
errors:
|
|
1370
|
+
word: state2.target,
|
|
1371
|
+
errors: state2.errorsThisWord,
|
|
990
1372
|
durationMs: now - session.current.wordStartedAt
|
|
991
1373
|
};
|
|
992
1374
|
const nextIndex = session.current.wordIndex + 1;
|
|
@@ -1013,7 +1395,7 @@ function feedSession(session, ev, now = Date.now()) {
|
|
|
1013
1395
|
return {
|
|
1014
1396
|
session: {
|
|
1015
1397
|
...session,
|
|
1016
|
-
current: { ...session.current, input:
|
|
1398
|
+
current: { ...session.current, input: state2 }
|
|
1017
1399
|
},
|
|
1018
1400
|
effect
|
|
1019
1401
|
};
|
|
@@ -1097,24 +1479,24 @@ function topN(book, n) {
|
|
|
1097
1479
|
}
|
|
1098
1480
|
|
|
1099
1481
|
// src/ui/hooks/useWordLoop.ts
|
|
1100
|
-
import { useEffect as useEffect3, useReducer, useRef, useState as
|
|
1482
|
+
import { useEffect as useEffect3, useReducer, useRef, useState as useState6 } from "react";
|
|
1101
1483
|
import { useInput as useInput2, useApp as useApp2 } from "ink";
|
|
1102
|
-
function reducer(
|
|
1484
|
+
function reducer(state2, action) {
|
|
1103
1485
|
if (action.type === "start") {
|
|
1104
1486
|
return { session: startSession(action.playlist, action.now), lastEffect: null };
|
|
1105
1487
|
}
|
|
1106
1488
|
if (action.type === "skip") {
|
|
1107
|
-
const r = skipSession(
|
|
1489
|
+
const r = skipSession(state2.session, action.now);
|
|
1108
1490
|
return { session: r.session, lastEffect: r.effect };
|
|
1109
1491
|
}
|
|
1110
1492
|
if (action.type === "event") {
|
|
1111
1493
|
if (action.key.backspace || action.key.delete) {
|
|
1112
|
-
const r = feedSession(
|
|
1494
|
+
const r = feedSession(state2.session, { type: "backspace" }, action.now);
|
|
1113
1495
|
return { session: r.session, lastEffect: r.effect };
|
|
1114
1496
|
}
|
|
1115
|
-
if (action.input.length === 0) return
|
|
1116
|
-
let session =
|
|
1117
|
-
let lastEffect =
|
|
1497
|
+
if (action.input.length === 0) return state2;
|
|
1498
|
+
let session = state2.session;
|
|
1499
|
+
let lastEffect = state2.lastEffect;
|
|
1118
1500
|
for (const c of action.input) {
|
|
1119
1501
|
const r = feedSession(session, { type: "char", ch: c }, action.now);
|
|
1120
1502
|
session = r.session;
|
|
@@ -1123,15 +1505,15 @@ function reducer(state, action) {
|
|
|
1123
1505
|
}
|
|
1124
1506
|
return { session, lastEffect };
|
|
1125
1507
|
}
|
|
1126
|
-
return
|
|
1508
|
+
return state2;
|
|
1127
1509
|
}
|
|
1128
1510
|
function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled = true }) {
|
|
1129
|
-
const [
|
|
1511
|
+
const [state2, dispatch] = useReducer(reducer, void 0, () => ({
|
|
1130
1512
|
session: startSession(playlist, Date.now()),
|
|
1131
1513
|
lastEffect: null
|
|
1132
1514
|
}));
|
|
1133
1515
|
const completedRef = useRef(false);
|
|
1134
|
-
const [tick, setTick] =
|
|
1516
|
+
const [tick, setTick] = useState6(0);
|
|
1135
1517
|
const { exit } = useApp2();
|
|
1136
1518
|
useInput2(
|
|
1137
1519
|
(input, key) => {
|
|
@@ -1164,17 +1546,17 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1164
1546
|
{ isActive: enabled }
|
|
1165
1547
|
);
|
|
1166
1548
|
useEffect3(() => {
|
|
1167
|
-
if (
|
|
1549
|
+
if (state2.session.finishedAt !== null && !completedRef.current) {
|
|
1168
1550
|
completedRef.current = true;
|
|
1169
|
-
onComplete(
|
|
1551
|
+
onComplete(state2.session);
|
|
1170
1552
|
}
|
|
1171
|
-
}, [
|
|
1553
|
+
}, [state2.session, onComplete]);
|
|
1172
1554
|
useEffect3(() => {
|
|
1173
|
-
if (
|
|
1555
|
+
if (state2.session.finishedAt !== null) return;
|
|
1174
1556
|
const id = setInterval(() => setTick((t) => t + 1), 1e3);
|
|
1175
1557
|
return () => clearInterval(id);
|
|
1176
|
-
}, [
|
|
1177
|
-
return { session:
|
|
1558
|
+
}, [state2.session.finishedAt]);
|
|
1559
|
+
return { session: state2.session, lastEffect: state2.lastEffect, tick };
|
|
1178
1560
|
}
|
|
1179
1561
|
|
|
1180
1562
|
// src/ui/hooks/useAudio.ts
|
|
@@ -1284,6 +1666,43 @@ function dailyBuckets(sessions, days, now = /* @__PURE__ */ new Date()) {
|
|
|
1284
1666
|
return out;
|
|
1285
1667
|
}
|
|
1286
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
|
+
|
|
1287
1706
|
// src/ui/hooks/useSessionPersistence.ts
|
|
1288
1707
|
function useSessionPersistence(meta) {
|
|
1289
1708
|
return useCallback3(
|
|
@@ -1299,6 +1718,15 @@ function useSessionPersistence(meta) {
|
|
|
1299
1718
|
perWordErrors: summary.perWordErrors
|
|
1300
1719
|
};
|
|
1301
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
|
+
});
|
|
1302
1730
|
const dirty = Object.entries(summary.perWordErrors).filter(([, n]) => n > 0);
|
|
1303
1731
|
if (dirty.length === 0) return;
|
|
1304
1732
|
let book = await loadMistakes();
|
|
@@ -1310,14 +1738,14 @@ function useSessionPersistence(meta) {
|
|
|
1310
1738
|
}
|
|
1311
1739
|
|
|
1312
1740
|
// src/ui/screens/PracticeScreen.tsx
|
|
1313
|
-
import { jsx as
|
|
1741
|
+
import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1314
1742
|
function PracticeScreen({ params }) {
|
|
1315
1743
|
const { dictId, chapterIndex, mode } = params;
|
|
1316
1744
|
const { cfg } = useAppState();
|
|
1317
|
-
const
|
|
1318
|
-
const [phase, setPhase] =
|
|
1319
|
-
const [loaded, setLoaded] =
|
|
1320
|
-
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);
|
|
1321
1749
|
useEffect5(() => {
|
|
1322
1750
|
let cancelled = false;
|
|
1323
1751
|
setPhase("loading");
|
|
@@ -1332,7 +1760,7 @@ function PracticeScreen({ params }) {
|
|
|
1332
1760
|
if (cancelled) return;
|
|
1333
1761
|
const reviewWords = words.filter((w) => book[w.name]?.count).slice(0, cfg.chapterSize);
|
|
1334
1762
|
if (reviewWords.length === 0) {
|
|
1335
|
-
setErrorMsg(
|
|
1763
|
+
setErrorMsg(t.practice.errors.noMistakes);
|
|
1336
1764
|
setPhase("error");
|
|
1337
1765
|
return;
|
|
1338
1766
|
}
|
|
@@ -1342,7 +1770,7 @@ function PracticeScreen({ params }) {
|
|
|
1342
1770
|
}
|
|
1343
1771
|
const chapters = chunkChapters(words, cfg.chapterSize);
|
|
1344
1772
|
if (chapters.length === 0) {
|
|
1345
|
-
setErrorMsg(
|
|
1773
|
+
setErrorMsg(t.practice.errors.dictEmpty(dictId));
|
|
1346
1774
|
setPhase("error");
|
|
1347
1775
|
return;
|
|
1348
1776
|
}
|
|
@@ -1359,15 +1787,15 @@ function PracticeScreen({ params }) {
|
|
|
1359
1787
|
return () => {
|
|
1360
1788
|
cancelled = true;
|
|
1361
1789
|
};
|
|
1362
|
-
}, [dictId, chapterIndex, mode, cfg.chapterSize]);
|
|
1790
|
+
}, [dictId, chapterIndex, mode, cfg.chapterSize, t]);
|
|
1363
1791
|
if (phase === "loading") {
|
|
1364
|
-
return /* @__PURE__ */
|
|
1792
|
+
return /* @__PURE__ */ jsx8(Centered, { text: t.practice.loading, color: PALETTE.muted });
|
|
1365
1793
|
}
|
|
1366
1794
|
if (phase === "error") {
|
|
1367
|
-
return /* @__PURE__ */
|
|
1795
|
+
return /* @__PURE__ */ jsx8(ErrorView, { msg: errorMsg ?? t.practice.errors.unknown });
|
|
1368
1796
|
}
|
|
1369
1797
|
if (!loaded) return null;
|
|
1370
|
-
return /* @__PURE__ */
|
|
1798
|
+
return /* @__PURE__ */ jsx8(
|
|
1371
1799
|
PracticeRunner,
|
|
1372
1800
|
{
|
|
1373
1801
|
params,
|
|
@@ -1468,9 +1896,9 @@ function PracticeRunner({
|
|
|
1468
1896
|
},
|
|
1469
1897
|
{ isActive: phase === "summary" }
|
|
1470
1898
|
);
|
|
1471
|
-
if (phase === "paused") return /* @__PURE__ */
|
|
1899
|
+
if (phase === "paused") return /* @__PURE__ */ jsx8(PausedView, {});
|
|
1472
1900
|
if (phase === "summary") {
|
|
1473
|
-
return /* @__PURE__ */
|
|
1901
|
+
return /* @__PURE__ */ jsx8(
|
|
1474
1902
|
SummaryView,
|
|
1475
1903
|
{
|
|
1476
1904
|
dictId,
|
|
@@ -1488,7 +1916,7 @@ function PracticeRunner({
|
|
|
1488
1916
|
const errors = session.results.reduce((a, r) => a + r.errors, 0);
|
|
1489
1917
|
const minutes = elapsedMs / 6e4;
|
|
1490
1918
|
const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
|
|
1491
|
-
return /* @__PURE__ */
|
|
1919
|
+
return /* @__PURE__ */ jsx8(
|
|
1492
1920
|
TypingLayout,
|
|
1493
1921
|
{
|
|
1494
1922
|
dictId,
|
|
@@ -1522,9 +1950,10 @@ function fmtTime(ms) {
|
|
|
1522
1950
|
return `${m}:${String(s).padStart(2, "0")}`;
|
|
1523
1951
|
}
|
|
1524
1952
|
function TypingLayout(props) {
|
|
1953
|
+
const t = useStrings();
|
|
1525
1954
|
const progressFrac = props.total === 0 ? 0 : props.completed / props.total;
|
|
1526
|
-
return /* @__PURE__ */ jsxs3(
|
|
1527
|
-
/* @__PURE__ */
|
|
1955
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
1956
|
+
/* @__PURE__ */ jsx8(
|
|
1528
1957
|
StatusBar,
|
|
1529
1958
|
{
|
|
1530
1959
|
dictId: props.dictId,
|
|
@@ -1537,8 +1966,8 @@ function TypingLayout(props) {
|
|
|
1537
1966
|
elapsedMs: props.elapsedMs
|
|
1538
1967
|
}
|
|
1539
1968
|
),
|
|
1540
|
-
/* @__PURE__ */ jsxs3(
|
|
1541
|
-
/* @__PURE__ */
|
|
1969
|
+
/* @__PURE__ */ jsxs3(Box4, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
|
|
1970
|
+
/* @__PURE__ */ jsx8(
|
|
1542
1971
|
BigWord,
|
|
1543
1972
|
{
|
|
1544
1973
|
target: props.target,
|
|
@@ -1547,12 +1976,12 @@ function TypingLayout(props) {
|
|
|
1547
1976
|
hideTarget: props.hideTarget
|
|
1548
1977
|
}
|
|
1549
1978
|
),
|
|
1550
|
-
props.phonetic && /* @__PURE__ */
|
|
1551
|
-
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)) })
|
|
1552
1981
|
] }),
|
|
1553
|
-
/* @__PURE__ */ jsxs3(
|
|
1554
|
-
/* @__PURE__ */
|
|
1555
|
-
/* @__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: [
|
|
1556
1985
|
props.completed,
|
|
1557
1986
|
"/",
|
|
1558
1987
|
props.total,
|
|
@@ -1560,21 +1989,27 @@ function TypingLayout(props) {
|
|
|
1560
1989
|
fmtTime(props.elapsedMs),
|
|
1561
1990
|
" \xB7 ",
|
|
1562
1991
|
props.wpm,
|
|
1563
|
-
"
|
|
1992
|
+
" ",
|
|
1993
|
+
t.practice.statCards.wpm,
|
|
1994
|
+
" \xB7 ",
|
|
1564
1995
|
props.errors,
|
|
1565
|
-
"
|
|
1996
|
+
" ",
|
|
1997
|
+
t.practice.statCards.errors
|
|
1566
1998
|
] }) }),
|
|
1567
|
-
/* @__PURE__ */
|
|
1999
|
+
/* @__PURE__ */ jsx8(Box4, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx8(Text3, { color: PALETTE.muted, children: t.practice.footers.typing }) })
|
|
1568
2000
|
] })
|
|
1569
2001
|
] });
|
|
1570
2002
|
}
|
|
1571
2003
|
function StatusBar(props) {
|
|
1572
|
-
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}`;
|
|
1573
2008
|
const right = `${props.completed}/${props.total} \xB7 ${fmtTime(props.elapsedMs)}`;
|
|
1574
|
-
return /* @__PURE__ */ jsxs3(
|
|
1575
|
-
/* @__PURE__ */
|
|
1576
|
-
/* @__PURE__ */
|
|
1577
|
-
/* @__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 })
|
|
1578
2013
|
] });
|
|
1579
2014
|
}
|
|
1580
2015
|
function ProgressBar({ frac }) {
|
|
@@ -1582,22 +2017,27 @@ function ProgressBar({ frac }) {
|
|
|
1582
2017
|
const width = Math.max(20, Math.min(72, cols - 16));
|
|
1583
2018
|
const filled = Math.round(width * Math.max(0, Math.min(1, frac)));
|
|
1584
2019
|
const empty = width - filled;
|
|
1585
|
-
return /* @__PURE__ */ jsxs3(
|
|
1586
|
-
/* @__PURE__ */
|
|
1587
|
-
/* @__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) })
|
|
1588
2023
|
] });
|
|
1589
2024
|
}
|
|
1590
2025
|
function PausedView() {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
/* @__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 }) })
|
|
1594
2030
|
] });
|
|
1595
2031
|
}
|
|
1596
2032
|
function ErrorView({ msg }) {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
/* @__PURE__ */
|
|
1600
|
-
/* @__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, {})
|
|
1601
2041
|
] });
|
|
1602
2042
|
}
|
|
1603
2043
|
function BackKey() {
|
|
@@ -1608,7 +2048,7 @@ function BackKey() {
|
|
|
1608
2048
|
return null;
|
|
1609
2049
|
}
|
|
1610
2050
|
function Centered({ text, color }) {
|
|
1611
|
-
return /* @__PURE__ */
|
|
2051
|
+
return /* @__PURE__ */ jsx8(Box4, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx8(Text3, { color, children: text }) });
|
|
1612
2052
|
}
|
|
1613
2053
|
function SummaryView(props) {
|
|
1614
2054
|
const { summary } = props;
|
|
@@ -1617,52 +2057,52 @@ function SummaryView(props) {
|
|
|
1617
2057
|
const errorWords = Object.keys(summary.perWordErrors).length;
|
|
1618
2058
|
const acc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - errorWords) / summary.wordCount);
|
|
1619
2059
|
const accPct = Math.round(acc * 1e3) / 10;
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
/* @__PURE__ */
|
|
1625
|
-
|
|
1626
|
-
|
|
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(
|
|
1627
2069
|
StatCard,
|
|
1628
2070
|
{
|
|
1629
|
-
label:
|
|
2071
|
+
label: t.practice.statCards.errors,
|
|
1630
2072
|
value: String(summary.errors),
|
|
1631
2073
|
color: summary.errors > 0 ? PALETTE.error : PALETTE.muted
|
|
1632
2074
|
}
|
|
1633
2075
|
),
|
|
1634
|
-
/* @__PURE__ */
|
|
1635
|
-
/* @__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 })
|
|
1636
2078
|
] }),
|
|
1637
|
-
/* @__PURE__ */
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
] }) }),
|
|
1641
|
-
/* @__PURE__ */ jsx7(Box3, { flexGrow: 1 }),
|
|
1642
|
-
/* @__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 }) })
|
|
1643
2082
|
] });
|
|
1644
2083
|
}
|
|
1645
2084
|
function StatCard({ label, value, color }) {
|
|
1646
|
-
return /* @__PURE__ */ jsxs3(
|
|
1647
|
-
/* @__PURE__ */
|
|
1648
|
-
/* @__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 })
|
|
1649
2088
|
] });
|
|
1650
2089
|
}
|
|
1651
2090
|
|
|
1652
2091
|
// src/ui/screens/DictBrowser.tsx
|
|
1653
|
-
import { useEffect as useEffect6, useState as
|
|
1654
|
-
import { Box as
|
|
1655
|
-
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";
|
|
1656
2095
|
function DictBrowser({ params }) {
|
|
1657
2096
|
const nav = useNav();
|
|
1658
2097
|
const { cfg, setCfg } = useAppState();
|
|
1659
|
-
const
|
|
1660
|
-
const [
|
|
1661
|
-
const [
|
|
1662
|
-
const [
|
|
1663
|
-
const [
|
|
1664
|
-
const [
|
|
1665
|
-
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);
|
|
1666
2106
|
const refresh = async () => {
|
|
1667
2107
|
const reg = await loadRegistry();
|
|
1668
2108
|
const flagged = await Promise.all(
|
|
@@ -1730,7 +2170,7 @@ function DictBrowser({ params }) {
|
|
|
1730
2170
|
try {
|
|
1731
2171
|
await removeDictionary(current.entry.id);
|
|
1732
2172
|
setPending(null);
|
|
1733
|
-
setTick((
|
|
2173
|
+
setTick((n) => n + 1);
|
|
1734
2174
|
} catch (err) {
|
|
1735
2175
|
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
1736
2176
|
}
|
|
@@ -1743,7 +2183,7 @@ function DictBrowser({ params }) {
|
|
|
1743
2183
|
try {
|
|
1744
2184
|
await pullDictionary(current.entry.id);
|
|
1745
2185
|
setPending(null);
|
|
1746
|
-
setTick((
|
|
2186
|
+
setTick((n) => n + 1);
|
|
1747
2187
|
} catch (err) {
|
|
1748
2188
|
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
1749
2189
|
}
|
|
@@ -1751,88 +2191,67 @@ function DictBrowser({ params }) {
|
|
|
1751
2191
|
}
|
|
1752
2192
|
});
|
|
1753
2193
|
if (loading) {
|
|
1754
|
-
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 }) });
|
|
1755
2195
|
}
|
|
1756
|
-
return /* @__PURE__ */ jsxs4(
|
|
1757
|
-
/* @__PURE__ */ jsxs4(
|
|
1758
|
-
/* @__PURE__ */
|
|
1759
|
-
/* @__PURE__ */
|
|
1760
|
-
/* @__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) })
|
|
1761
2201
|
] }),
|
|
1762
|
-
/* @__PURE__ */ jsxs4(
|
|
1763
|
-
/* @__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) => {
|
|
1764
2204
|
const i = Math.max(0, safeSelected - 8) + vi;
|
|
1765
2205
|
const active = i === safeSelected;
|
|
1766
2206
|
const isDefault = cfg.defaultDict === row.entry.id;
|
|
1767
|
-
return /* @__PURE__ */ jsxs4(
|
|
1768
|
-
/* @__PURE__ */
|
|
1769
|
-
/* @__PURE__ */
|
|
1770
|
-
/* @__PURE__ */
|
|
1771
|
-
/* @__PURE__ */
|
|
1772
|
-
/* @__PURE__ */
|
|
1773
|
-
/* @__PURE__ */ jsx8(Text4, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: row.entry.id.slice(0, 14).padEnd(15) }),
|
|
1774
|
-
/* @__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) }) })
|
|
1775
2213
|
] }, row.entry.id);
|
|
1776
2214
|
}) }),
|
|
1777
|
-
/* @__PURE__ */
|
|
1778
|
-
/* @__PURE__ */
|
|
1779
|
-
/* @__PURE__ */
|
|
1780
|
-
/* @__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: [
|
|
1781
2219
|
current.entry.language,
|
|
1782
2220
|
" \xB7 ",
|
|
1783
|
-
current.entry.category
|
|
1784
|
-
" \xB7 ",
|
|
1785
|
-
current.entry.length,
|
|
1786
|
-
" words"
|
|
2221
|
+
current.entry.category
|
|
1787
2222
|
] }) }),
|
|
1788
|
-
/* @__PURE__ */
|
|
1789
|
-
current.entry.
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
|
|
1794
|
-
/* @__PURE__ */ jsx8(Text4, { color: current.local ? PALETTE.accent : PALETTE.muted, children: current.local ? "local \u2713" : "not local" }),
|
|
1795
|
-
cfg.defaultDict === current.entry.id && /* @__PURE__ */ jsx8(Text4, { color: PALETTE.success, children: " \xB7 default \u2605" })
|
|
1796
|
-
] })
|
|
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 }) })
|
|
1797
2228
|
] }) })
|
|
1798
2229
|
] }),
|
|
1799
|
-
pending && /* @__PURE__ */ jsxs4(
|
|
1800
|
-
pending.kind === "pulling" && /* @__PURE__ */
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
"\u2026"
|
|
1804
|
-
] }),
|
|
1805
|
-
pending.kind === "removing" && /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.warning, children: [
|
|
1806
|
-
"removing ",
|
|
1807
|
-
pending.id,
|
|
1808
|
-
"\u2026"
|
|
1809
|
-
] }),
|
|
1810
|
-
pending.kind === "error" && /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.error, children: [
|
|
1811
|
-
"error on ",
|
|
1812
|
-
pending.id,
|
|
1813
|
-
": ",
|
|
1814
|
-
pending.msg
|
|
1815
|
-
] })
|
|
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) })
|
|
1816
2234
|
] }),
|
|
1817
|
-
/* @__PURE__ */
|
|
2235
|
+
/* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text4, { color: PALETTE.muted, children: t.dict.footer }) })
|
|
1818
2236
|
] });
|
|
1819
2237
|
}
|
|
1820
2238
|
|
|
1821
2239
|
// src/ui/screens/ConfigEditor.tsx
|
|
1822
|
-
import { useState as
|
|
1823
|
-
import { Box as
|
|
1824
|
-
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";
|
|
1825
2243
|
var FIELDS = [
|
|
1826
|
-
{ kind: "dictRef", path: "defaultDict",
|
|
1827
|
-
{ kind: "enum", path: "defaultMode",
|
|
1828
|
-
{ kind: "enum", path: "accent",
|
|
1829
|
-
{ kind: "enum", path: "
|
|
1830
|
-
{ kind: "
|
|
1831
|
-
{ kind: "
|
|
1832
|
-
{ kind: "bool", path: "
|
|
1833
|
-
{ kind: "bool", path: "sounds.
|
|
1834
|
-
{ kind: "bool", path: "sounds.
|
|
1835
|
-
{ 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" }
|
|
1836
2255
|
];
|
|
1837
2256
|
function getByPath2(cfg, path) {
|
|
1838
2257
|
return path.split(".").reduce((acc, k) => {
|
|
@@ -1843,10 +2262,11 @@ function getByPath2(cfg, path) {
|
|
|
1843
2262
|
function ConfigEditor() {
|
|
1844
2263
|
const nav = useNav();
|
|
1845
2264
|
const { cfg, setCfg } = useAppState();
|
|
1846
|
-
const
|
|
1847
|
-
const [
|
|
1848
|
-
const [
|
|
1849
|
-
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);
|
|
1850
2270
|
const field = FIELDS[selected];
|
|
1851
2271
|
const currentValue = getByPath2(cfg, field.path);
|
|
1852
2272
|
const commit = async (raw) => {
|
|
@@ -1929,50 +2349,57 @@ function ConfigEditor() {
|
|
|
1929
2349
|
setError(null);
|
|
1930
2350
|
}
|
|
1931
2351
|
});
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
/* @__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) => {
|
|
1935
2356
|
const active = i === selected;
|
|
1936
2357
|
const value = getByPath2(cfg, f.path);
|
|
1937
|
-
const display = renderValue(f, value, active && editing ? draft : null);
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
/* @__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 })
|
|
1942
2368
|
] }, f.path);
|
|
1943
2369
|
}) }),
|
|
1944
|
-
error && /* @__PURE__ */
|
|
2370
|
+
error && /* @__PURE__ */ jsx10(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: PALETTE.error, children: [
|
|
1945
2371
|
"! ",
|
|
1946
2372
|
error
|
|
1947
2373
|
] }) }),
|
|
1948
|
-
/* @__PURE__ */
|
|
2374
|
+
/* @__PURE__ */ jsx10(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text5, { color: PALETTE.muted, children: hintFor(field, editing, t) }) })
|
|
1949
2375
|
] });
|
|
1950
2376
|
}
|
|
1951
|
-
function renderValue(field, value, draft) {
|
|
2377
|
+
function renderValue(field, value, draft, t) {
|
|
1952
2378
|
if (draft !== null) return `${draft}_`;
|
|
1953
|
-
if (field.kind === "bool") return value ?
|
|
1954
|
-
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");
|
|
1955
2381
|
if (field.kind === "enum") return `< ${value} >`;
|
|
1956
2382
|
return String(value ?? "");
|
|
1957
2383
|
}
|
|
1958
|
-
function hintFor(field, editing) {
|
|
1959
|
-
if (editing) return
|
|
1960
|
-
if (field.kind === "bool") return
|
|
1961
|
-
if (field.kind === "enum") return
|
|
1962
|
-
if (field.kind === "dictRef") return
|
|
1963
|
-
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;
|
|
1964
2390
|
}
|
|
1965
2391
|
|
|
1966
2392
|
// src/ui/screens/StatsViewer.tsx
|
|
1967
|
-
import { useEffect as useEffect7, useState as
|
|
1968
|
-
import { Box as
|
|
1969
|
-
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";
|
|
1970
2396
|
var DAY_WINDOWS = [7, 14, 30, 90];
|
|
1971
2397
|
function StatsViewer() {
|
|
1972
2398
|
const nav = useNav();
|
|
1973
|
-
const
|
|
1974
|
-
const [
|
|
1975
|
-
const [
|
|
2399
|
+
const t = useStrings();
|
|
2400
|
+
const [sessions, setSessions] = useState10(null);
|
|
2401
|
+
const [book, setBook] = useState10(null);
|
|
2402
|
+
const [windowIdx, setWindowIdx] = useState10(1);
|
|
1976
2403
|
useEffect7(() => {
|
|
1977
2404
|
void (async () => {
|
|
1978
2405
|
const [s, b] = await Promise.all([loadSessions(), loadMistakes()]);
|
|
@@ -1989,13 +2416,16 @@ function StatsViewer() {
|
|
|
1989
2416
|
if (input === "N") setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);
|
|
1990
2417
|
});
|
|
1991
2418
|
if (!sessions || !book) {
|
|
1992
|
-
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 }) });
|
|
1993
2420
|
}
|
|
1994
2421
|
if (sessions.length === 0) {
|
|
1995
|
-
return /* @__PURE__ */ jsxs6(
|
|
1996
|
-
/* @__PURE__ */
|
|
1997
|
-
/* @__PURE__ */
|
|
1998
|
-
/* @__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
|
+
] }) })
|
|
1999
2429
|
] });
|
|
2000
2430
|
}
|
|
2001
2431
|
const days = DAY_WINDOWS[windowIdx];
|
|
@@ -2012,53 +2442,49 @@ function StatsViewer() {
|
|
|
2012
2442
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
2013
2443
|
const recent = sessions.slice(-5).reverse();
|
|
2014
2444
|
const top = topN(book, 8);
|
|
2015
|
-
return /* @__PURE__ */ jsxs6(
|
|
2016
|
-
/* @__PURE__ */
|
|
2017
|
-
/* @__PURE__ */ jsxs6(
|
|
2018
|
-
/* @__PURE__ */
|
|
2019
|
-
/* @__PURE__ */ jsxs6(
|
|
2020
|
-
/* @__PURE__ */
|
|
2021
|
-
/* @__PURE__ */
|
|
2022
|
-
/* @__PURE__ */
|
|
2023
|
-
/* @__PURE__ */
|
|
2024
|
-
/* @__PURE__ */
|
|
2025
|
-
/* @__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 })
|
|
2026
2456
|
] })
|
|
2027
2457
|
] }),
|
|
2028
|
-
/* @__PURE__ */ jsxs6(
|
|
2029
|
-
/* @__PURE__ */
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2035
|
-
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
2036
|
-
/* @__PURE__ */ jsx10(Text6, { color: PALETTE.muted, children: "wpm ".padEnd(10) }),
|
|
2037
|
-
/* @__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)) }),
|
|
2038
2464
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2039
2465
|
" max ",
|
|
2040
2466
|
Math.round(Math.max(...buckets.map((b) => b.wpm)))
|
|
2041
2467
|
] })
|
|
2042
2468
|
] }),
|
|
2043
|
-
/* @__PURE__ */ jsxs6(
|
|
2044
|
-
/* @__PURE__ */
|
|
2045
|
-
/* @__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)) })
|
|
2046
2472
|
] }),
|
|
2047
|
-
/* @__PURE__ */ jsxs6(
|
|
2048
|
-
/* @__PURE__ */
|
|
2049
|
-
/* @__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)) })
|
|
2050
2476
|
] })
|
|
2051
2477
|
] })
|
|
2052
2478
|
] }),
|
|
2053
|
-
/* @__PURE__ */ jsxs6(
|
|
2054
|
-
/* @__PURE__ */
|
|
2055
|
-
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: [
|
|
2056
2482
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2057
2483
|
" ",
|
|
2058
2484
|
s.ts.replace("T", " ").slice(0, 16),
|
|
2059
2485
|
" "
|
|
2060
2486
|
] }),
|
|
2061
|
-
/* @__PURE__ */
|
|
2487
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.text, children: s.dictId.padEnd(14) }),
|
|
2062
2488
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2063
2489
|
" ",
|
|
2064
2490
|
"ch",
|
|
@@ -2077,37 +2503,37 @@ function StatsViewer() {
|
|
|
2077
2503
|
] })
|
|
2078
2504
|
] }, i))
|
|
2079
2505
|
] }),
|
|
2080
|
-
top.length > 0 && /* @__PURE__ */ jsxs6(
|
|
2081
|
-
/* @__PURE__ */
|
|
2082
|
-
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: [
|
|
2083
2509
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.error, children: [
|
|
2084
2510
|
" ",
|
|
2085
2511
|
String(entry.count).padStart(3),
|
|
2086
2512
|
" "
|
|
2087
2513
|
] }),
|
|
2088
|
-
/* @__PURE__ */
|
|
2089
|
-
/* @__PURE__ */
|
|
2514
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.text, children: word.padEnd(20) }),
|
|
2515
|
+
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: entry.dictIds.join(", ") })
|
|
2090
2516
|
] }, word))
|
|
2091
2517
|
] }),
|
|
2092
|
-
/* @__PURE__ */
|
|
2093
|
-
/* @__PURE__ */
|
|
2518
|
+
/* @__PURE__ */ jsx11(Box7, { flexGrow: 1 }),
|
|
2519
|
+
/* @__PURE__ */ jsx11(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.footer }) })
|
|
2094
2520
|
] });
|
|
2095
2521
|
}
|
|
2096
2522
|
function Stat({ label, value, accent = false }) {
|
|
2097
|
-
return /* @__PURE__ */ jsxs6(
|
|
2523
|
+
return /* @__PURE__ */ jsxs6(Box7, { marginRight: 3, children: [
|
|
2098
2524
|
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2099
2525
|
label,
|
|
2100
2526
|
" "
|
|
2101
2527
|
] }),
|
|
2102
|
-
/* @__PURE__ */
|
|
2528
|
+
/* @__PURE__ */ jsx11(Text6, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
|
|
2103
2529
|
] });
|
|
2104
2530
|
}
|
|
2105
2531
|
|
|
2106
2532
|
// src/ui/screens/WordLookup.tsx
|
|
2107
|
-
import { useEffect as useEffect8, useState as
|
|
2108
|
-
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";
|
|
2109
2535
|
import { readdir } from "fs/promises";
|
|
2110
|
-
import { Fragment as
|
|
2536
|
+
import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2111
2537
|
async function listLocalDictIds() {
|
|
2112
2538
|
try {
|
|
2113
2539
|
const files = await readdir(paths.dictsDir);
|
|
@@ -2118,11 +2544,12 @@ async function listLocalDictIds() {
|
|
|
2118
2544
|
}
|
|
2119
2545
|
function WordLookup() {
|
|
2120
2546
|
const nav = useNav();
|
|
2121
|
-
const
|
|
2122
|
-
const [
|
|
2123
|
-
const [
|
|
2124
|
-
const [
|
|
2125
|
-
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);
|
|
2126
2553
|
useEffect8(() => {
|
|
2127
2554
|
void (async () => {
|
|
2128
2555
|
const ids = await listLocalDictIds();
|
|
@@ -2163,49 +2590,45 @@ function WordLookup() {
|
|
|
2163
2590
|
}
|
|
2164
2591
|
});
|
|
2165
2592
|
if (loading) {
|
|
2166
|
-
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 }) });
|
|
2167
2594
|
}
|
|
2168
2595
|
if (allWords.length === 0) {
|
|
2169
|
-
return /* @__PURE__ */ jsxs7(
|
|
2170
|
-
/* @__PURE__ */
|
|
2171
|
-
/* @__PURE__ */
|
|
2172
|
-
/* @__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
|
+
] }) })
|
|
2173
2603
|
] });
|
|
2174
2604
|
}
|
|
2175
2605
|
const current = filtered[selected];
|
|
2176
|
-
return /* @__PURE__ */ jsxs7(
|
|
2177
|
-
/* @__PURE__ */ jsxs7(
|
|
2178
|
-
/* @__PURE__ */
|
|
2179
|
-
/* @__PURE__ */
|
|
2180
|
-
/* @__PURE__ */
|
|
2181
|
-
allWords.length,
|
|
2182
|
-
" words across local dicts"
|
|
2183
|
-
] })
|
|
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) })
|
|
2184
2611
|
] }),
|
|
2185
|
-
/* @__PURE__ */ jsxs7(
|
|
2186
|
-
/* @__PURE__ */
|
|
2187
|
-
/* @__PURE__ */
|
|
2188
|
-
/* @__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: "_" })
|
|
2189
2616
|
] }),
|
|
2190
|
-
/* @__PURE__ */ jsxs7(
|
|
2191
|
-
/* @__PURE__ */ jsxs7(
|
|
2617
|
+
/* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexGrow: 1, children: [
|
|
2618
|
+
/* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", width: "40%", children: [
|
|
2192
2619
|
filtered.map((h, i) => {
|
|
2193
2620
|
const active = i === selected;
|
|
2194
|
-
return /* @__PURE__ */ jsxs7(
|
|
2195
|
-
/* @__PURE__ */
|
|
2196
|
-
/* @__PURE__ */
|
|
2197
|
-
/* @__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 })
|
|
2198
2625
|
] }, `${h.dictId}-${h.word.name}-${i}`);
|
|
2199
2626
|
}),
|
|
2200
|
-
filtered.length === 0 && q && /* @__PURE__ */
|
|
2201
|
-
'no matches for "',
|
|
2202
|
-
query,
|
|
2203
|
-
'"'
|
|
2204
|
-
] })
|
|
2627
|
+
filtered.length === 0 && q && /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.noMatches(query) })
|
|
2205
2628
|
] }),
|
|
2206
|
-
/* @__PURE__ */
|
|
2207
|
-
/* @__PURE__ */
|
|
2208
|
-
/* @__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: [
|
|
2209
2632
|
current.word.usphone && /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
|
|
2210
2633
|
"US /",
|
|
2211
2634
|
current.word.usphone,
|
|
@@ -2217,59 +2640,109 @@ function WordLookup() {
|
|
|
2217
2640
|
"/"
|
|
2218
2641
|
] })
|
|
2219
2642
|
] }),
|
|
2220
|
-
/* @__PURE__ */
|
|
2643
|
+
/* @__PURE__ */ jsx12(Box8, { marginTop: 1, flexDirection: "column", children: (current.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.primary, children: [
|
|
2221
2644
|
"\xB7 ",
|
|
2222
|
-
|
|
2645
|
+
tr
|
|
2223
2646
|
] }, i)) }),
|
|
2224
|
-
/* @__PURE__ */
|
|
2225
|
-
|
|
2226
|
-
current.dictId
|
|
2227
|
-
] }) }),
|
|
2228
|
-
book[current.word.name] && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
2229
|
-
/* @__PURE__ */ jsxs7(Text7, { color: PALETTE.error, children: [
|
|
2230
|
-
"mistakes: ",
|
|
2231
|
-
book[current.word.name].count
|
|
2232
|
-
] }),
|
|
2233
|
-
/* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
|
|
2234
|
-
" ",
|
|
2235
|
-
"(last ",
|
|
2236
|
-
book[current.word.name].lastSeen.slice(0, 10),
|
|
2237
|
-
")"
|
|
2238
|
-
] })
|
|
2239
|
-
] })
|
|
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)) }) })
|
|
2240
2649
|
] }) })
|
|
2241
2650
|
] }),
|
|
2242
|
-
/* @__PURE__ */
|
|
2651
|
+
/* @__PURE__ */ jsx12(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.footer }) })
|
|
2243
2652
|
] });
|
|
2244
2653
|
}
|
|
2245
2654
|
|
|
2246
2655
|
// src/ui/App.tsx
|
|
2247
|
-
import { jsx as
|
|
2656
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
2248
2657
|
function App({ initial, initialCfg }) {
|
|
2249
|
-
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;
|
|
2250
2670
|
}
|
|
2251
2671
|
function Router() {
|
|
2252
2672
|
const nav = useNav();
|
|
2253
2673
|
const { cfg } = useAppState();
|
|
2254
2674
|
const { exit } = useApp4();
|
|
2255
|
-
|
|
2256
|
-
|
|
2675
|
+
const lastKeyRef = useRef4(null);
|
|
2676
|
+
useInput8((input, key2) => {
|
|
2677
|
+
if (key2.ctrl && input === "c") exit();
|
|
2257
2678
|
});
|
|
2258
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
|
+
}
|
|
2259
2685
|
switch (frame.name) {
|
|
2260
2686
|
case "main":
|
|
2261
|
-
return /* @__PURE__ */
|
|
2687
|
+
return /* @__PURE__ */ jsx13(MainMenu, { cfg });
|
|
2262
2688
|
case "practice":
|
|
2263
|
-
return /* @__PURE__ */
|
|
2689
|
+
return /* @__PURE__ */ jsx13(PracticeScreen, { params: frame.params });
|
|
2264
2690
|
case "dict":
|
|
2265
|
-
return /* @__PURE__ */
|
|
2691
|
+
return /* @__PURE__ */ jsx13(DictBrowser, { params: frame.params });
|
|
2266
2692
|
case "config":
|
|
2267
|
-
return /* @__PURE__ */
|
|
2693
|
+
return /* @__PURE__ */ jsx13(ConfigEditor, {});
|
|
2268
2694
|
case "stats":
|
|
2269
|
-
return /* @__PURE__ */
|
|
2695
|
+
return /* @__PURE__ */ jsx13(StatsViewer, {});
|
|
2270
2696
|
case "word":
|
|
2271
|
-
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}`);
|
|
2272
2742
|
}
|
|
2743
|
+
console.log();
|
|
2744
|
+
console.log(chalk3.dim(` ${t.report.farewell}`));
|
|
2745
|
+
console.log();
|
|
2273
2746
|
}
|
|
2274
2747
|
|
|
2275
2748
|
// src/commands/practice.ts
|
|
@@ -2279,24 +2752,25 @@ function isMode(v) {
|
|
|
2279
2752
|
}
|
|
2280
2753
|
async function runPractice(dictIdArg, options) {
|
|
2281
2754
|
if (!process.stdout.isTTY) {
|
|
2282
|
-
console.error(
|
|
2755
|
+
console.error(chalk4.red("Practice requires an interactive TTY."));
|
|
2283
2756
|
process.exitCode = 1;
|
|
2284
2757
|
return;
|
|
2285
2758
|
}
|
|
2286
2759
|
const cfg = await loadConfig();
|
|
2287
2760
|
const dictId = dictIdArg ?? cfg.defaultDict;
|
|
2288
2761
|
if (!dictId) {
|
|
2289
|
-
console.error(
|
|
2762
|
+
console.error(chalk4.red("No dictionary specified. Pass an id or set config.defaultDict."));
|
|
2290
2763
|
process.exitCode = 1;
|
|
2291
2764
|
return;
|
|
2292
2765
|
}
|
|
2293
2766
|
const mode = options.mode ?? cfg.defaultMode;
|
|
2294
2767
|
if (!isMode(mode)) {
|
|
2295
|
-
console.error(
|
|
2768
|
+
console.error(chalk4.red(`Invalid mode "${mode}". Valid: ${MODES.join(", ")}`));
|
|
2296
2769
|
process.exitCode = 1;
|
|
2297
2770
|
return;
|
|
2298
2771
|
}
|
|
2299
2772
|
const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);
|
|
2773
|
+
start();
|
|
2300
2774
|
const { waitUntilExit } = render(
|
|
2301
2775
|
createElement(App, {
|
|
2302
2776
|
initial: { name: "practice", params: { dictId, chapterIndex, mode } },
|
|
@@ -2305,6 +2779,9 @@ async function runPractice(dictIdArg, options) {
|
|
|
2305
2779
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
2306
2780
|
);
|
|
2307
2781
|
await waitUntilExit();
|
|
2782
|
+
ensureMainScreen();
|
|
2783
|
+
const { lang, t } = pickStrings(cfg.language);
|
|
2784
|
+
printSessionReport(report(), t, lang);
|
|
2308
2785
|
}
|
|
2309
2786
|
function buildPracticeCommand() {
|
|
2310
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) => {
|
|
@@ -2314,7 +2791,7 @@ function buildPracticeCommand() {
|
|
|
2314
2791
|
|
|
2315
2792
|
// src/commands/stats.ts
|
|
2316
2793
|
import { Command as Command4 } from "commander";
|
|
2317
|
-
import
|
|
2794
|
+
import chalk5 from "chalk";
|
|
2318
2795
|
function buildStatsCommand() {
|
|
2319
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) => {
|
|
2320
2797
|
const days = Math.max(1, Number(opts.days) || 14);
|
|
@@ -2322,7 +2799,7 @@ function buildStatsCommand() {
|
|
|
2322
2799
|
const sessions = await loadSessions();
|
|
2323
2800
|
const book = await loadMistakes();
|
|
2324
2801
|
if (sessions.length === 0) {
|
|
2325
|
-
console.log(
|
|
2802
|
+
console.log(chalk5.yellow("No practice history yet. Run `qwerty practice <dict>` to get started."));
|
|
2326
2803
|
return;
|
|
2327
2804
|
}
|
|
2328
2805
|
const buckets = dailyBuckets(sessions, days);
|
|
@@ -2336,33 +2813,33 @@ function buildStatsCommand() {
|
|
|
2336
2813
|
);
|
|
2337
2814
|
const overallWpm = totalMs > 0 ? Math.round(totalWords / (totalMs / 6e4) * 10) / 10 : 0;
|
|
2338
2815
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
2339
|
-
console.log(
|
|
2340
|
-
console.log(` ${
|
|
2341
|
-
console.log(` ${
|
|
2342
|
-
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(`
|
|
2343
2820
|
Last ${days} days`));
|
|
2344
|
-
console.log(` ${
|
|
2345
|
-
console.log(` ${
|
|
2346
|
-
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))}`);
|
|
2347
2824
|
const recent = sessions.slice(-5).reverse();
|
|
2348
|
-
console.log(
|
|
2825
|
+
console.log(chalk5.bold("\nLast 5 sessions"));
|
|
2349
2826
|
for (const s of recent) {
|
|
2350
2827
|
const wpm = computeWPM(s);
|
|
2351
2828
|
const acc = Math.round(accuracy(s) * 1e3) / 10;
|
|
2352
2829
|
console.log(
|
|
2353
|
-
` ${
|
|
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}%`
|
|
2354
2831
|
);
|
|
2355
2832
|
}
|
|
2356
2833
|
const top = topN(book, topCount);
|
|
2357
2834
|
if (top.length > 0) {
|
|
2358
|
-
console.log(
|
|
2835
|
+
console.log(chalk5.bold(`
|
|
2359
2836
|
Top ${top.length} mistakes`));
|
|
2360
2837
|
for (const [word, entry] of top) {
|
|
2361
|
-
console.log(` ${
|
|
2838
|
+
console.log(` ${chalk5.red(String(entry.count).padStart(3))} ${chalk5.bold(word.padEnd(20))} ${chalk5.dim(entry.dictIds.join(", "))}`);
|
|
2362
2839
|
}
|
|
2363
2840
|
} else {
|
|
2364
|
-
console.log(
|
|
2365
|
-
console.log(
|
|
2841
|
+
console.log(chalk5.bold("\nTop mistakes"));
|
|
2842
|
+
console.log(chalk5.dim(" none \u2014 keep going"));
|
|
2366
2843
|
}
|
|
2367
2844
|
console.log();
|
|
2368
2845
|
});
|
|
@@ -2370,7 +2847,7 @@ Top ${top.length} mistakes`));
|
|
|
2370
2847
|
|
|
2371
2848
|
// src/commands/word.ts
|
|
2372
2849
|
import { Command as Command5 } from "commander";
|
|
2373
|
-
import
|
|
2850
|
+
import chalk6 from "chalk";
|
|
2374
2851
|
import { readdir as readdir2 } from "fs/promises";
|
|
2375
2852
|
async function listLocalDictIds2() {
|
|
2376
2853
|
try {
|
|
@@ -2385,7 +2862,7 @@ function buildWordCommand() {
|
|
|
2385
2862
|
const q = keyword.toLowerCase();
|
|
2386
2863
|
const ids = await listLocalDictIds2();
|
|
2387
2864
|
if (ids.length === 0) {
|
|
2388
|
-
console.log(
|
|
2865
|
+
console.log(chalk6.yellow("No local dictionaries. Run `qwerty dict pull <id>` first."));
|
|
2389
2866
|
return;
|
|
2390
2867
|
}
|
|
2391
2868
|
const hits = [];
|
|
@@ -2399,7 +2876,7 @@ function buildWordCommand() {
|
|
|
2399
2876
|
}
|
|
2400
2877
|
}
|
|
2401
2878
|
if (hits.length === 0) {
|
|
2402
|
-
console.log(
|
|
2879
|
+
console.log(chalk6.yellow(`No matches for "${keyword}" in ${ids.length} local dictionaries`));
|
|
2403
2880
|
return;
|
|
2404
2881
|
}
|
|
2405
2882
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2412,21 +2889,21 @@ function buildWordCommand() {
|
|
|
2412
2889
|
for (const [name, group] of byName) {
|
|
2413
2890
|
const first = group[0].word;
|
|
2414
2891
|
console.log();
|
|
2415
|
-
console.log(
|
|
2892
|
+
console.log(chalk6.bold.white(name));
|
|
2416
2893
|
const us = first.usphone ? `US /${first.usphone}/` : "";
|
|
2417
2894
|
const uk = first.ukphone ? `UK /${first.ukphone}/` : "";
|
|
2418
|
-
if (us || uk) console.log(
|
|
2419
|
-
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}`));
|
|
2420
2897
|
const sources = await Promise.all(
|
|
2421
2898
|
group.map(async (h) => {
|
|
2422
2899
|
const reg = await findEntry(h.dictId);
|
|
2423
2900
|
return reg?.name ?? h.dictId;
|
|
2424
2901
|
})
|
|
2425
2902
|
);
|
|
2426
|
-
console.log(
|
|
2903
|
+
console.log(chalk6.dim(` in: ${sources.join(", ")}`));
|
|
2427
2904
|
const mistake = book[name];
|
|
2428
2905
|
if (mistake) {
|
|
2429
|
-
console.log(
|
|
2906
|
+
console.log(chalk6.dim(` mistakes: ${mistake.count} (last ${mistake.lastSeen.slice(0, 10)})`));
|
|
2430
2907
|
}
|
|
2431
2908
|
}
|
|
2432
2909
|
console.log();
|
|
@@ -2442,11 +2919,15 @@ async function runMainMenu() {
|
|
|
2442
2919
|
return;
|
|
2443
2920
|
}
|
|
2444
2921
|
const cfg = await loadConfig();
|
|
2922
|
+
start();
|
|
2445
2923
|
const { waitUntilExit } = render2(
|
|
2446
2924
|
createElement2(App, { initial: { name: "main" }, initialCfg: cfg }),
|
|
2447
2925
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
2448
2926
|
);
|
|
2449
2927
|
await waitUntilExit();
|
|
2928
|
+
ensureMainScreen();
|
|
2929
|
+
const { lang, t } = pickStrings(cfg.language);
|
|
2930
|
+
printSessionReport(report(), t, lang);
|
|
2450
2931
|
}
|
|
2451
2932
|
|
|
2452
2933
|
// src/cli.ts
|