qwerty-cli 0.0.1-alpha.5 → 0.0.1-alpha.7
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 +1759 -571
- package/dist/cli.js.map +1 -1
- package/package.json +1 -2
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/cli.ts
|
|
2
|
-
import { Command as
|
|
2
|
+
import { Command as Command7 } from "commander";
|
|
3
3
|
|
|
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.7",
|
|
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,9 @@ 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
|
+
stealth: z.enum(["off", "menu", "default"]).default("off")
|
|
154
155
|
});
|
|
155
156
|
var DEFAULTS = ConfigSchema.parse({});
|
|
156
157
|
async function loadConfig() {
|
|
@@ -506,12 +507,13 @@ ${matches.length} matches`));
|
|
|
506
507
|
|
|
507
508
|
// src/commands/practice.ts
|
|
508
509
|
import { Command as Command3 } from "commander";
|
|
509
|
-
import
|
|
510
|
+
import chalk4 from "chalk";
|
|
510
511
|
import { render } from "ink";
|
|
511
512
|
import { createElement } from "react";
|
|
512
513
|
|
|
513
514
|
// src/ui/App.tsx
|
|
514
|
-
import {
|
|
515
|
+
import { useRef as useRef4 } from "react";
|
|
516
|
+
import { useApp as useApp4, useInput as useInput10 } from "ink";
|
|
515
517
|
|
|
516
518
|
// src/ui/nav.tsx
|
|
517
519
|
import { createContext, useContext, useState, useCallback } from "react";
|
|
@@ -541,17 +543,27 @@ function useNav() {
|
|
|
541
543
|
}
|
|
542
544
|
|
|
543
545
|
// src/ui/Fullscreen.tsx
|
|
544
|
-
import { useEffect } from "react";
|
|
545
|
-
import {
|
|
546
|
-
|
|
546
|
+
import { useEffect, useState as useState2 } from "react";
|
|
547
|
+
import { Box, useStdout } from "ink";
|
|
548
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
549
|
+
var ENTER = "\x1B[?1049h\x1B[?25l\x1B[2J\x1B[H";
|
|
547
550
|
var LEAVE = "\x1B[?25h\x1B[?1049l";
|
|
548
551
|
function shouldUse() {
|
|
549
552
|
return Boolean(process.stdout.isTTY) && process.env.QWERTY_NO_ALTSCREEN !== "1";
|
|
550
553
|
}
|
|
551
554
|
function Fullscreen({ children }) {
|
|
555
|
+
const { stdout } = useStdout();
|
|
556
|
+
const [size, setSize] = useState2(() => ({
|
|
557
|
+
rows: stdout?.rows ?? 24,
|
|
558
|
+
cols: stdout?.columns ?? 80
|
|
559
|
+
}));
|
|
552
560
|
useEffect(() => {
|
|
553
561
|
if (!shouldUse()) return;
|
|
554
562
|
process.stdout.write(ENTER);
|
|
563
|
+
const onResize = () => {
|
|
564
|
+
setSize({ rows: process.stdout.rows ?? 24, cols: process.stdout.columns ?? 80 });
|
|
565
|
+
};
|
|
566
|
+
process.stdout.on("resize", onResize);
|
|
555
567
|
const leave = () => {
|
|
556
568
|
try {
|
|
557
569
|
process.stdout.write(LEAVE);
|
|
@@ -569,14 +581,15 @@ function Fullscreen({ children }) {
|
|
|
569
581
|
process.off("SIGINT", onSignal);
|
|
570
582
|
process.off("SIGTERM", onSignal);
|
|
571
583
|
process.off("exit", leave);
|
|
584
|
+
process.stdout.off("resize", onResize);
|
|
572
585
|
leave();
|
|
573
586
|
};
|
|
574
587
|
}, []);
|
|
575
|
-
return /* @__PURE__ */ jsx2(
|
|
588
|
+
return /* @__PURE__ */ jsx2(Box, { width: size.cols, height: size.rows, flexDirection: "column", children });
|
|
576
589
|
}
|
|
577
590
|
|
|
578
591
|
// src/ui/audio-context.tsx
|
|
579
|
-
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useState as
|
|
592
|
+
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useState as useState3 } from "react";
|
|
580
593
|
|
|
581
594
|
// src/infra/audio.ts
|
|
582
595
|
import { spawn } from "child_process";
|
|
@@ -738,7 +751,7 @@ function AudioStatusProvider({
|
|
|
738
751
|
disabled,
|
|
739
752
|
children
|
|
740
753
|
}) {
|
|
741
|
-
const [status, setStatus] =
|
|
754
|
+
const [status, setStatus] = useState3({ warning: null, ready: false });
|
|
742
755
|
useEffect2(() => {
|
|
743
756
|
let cancelled = false;
|
|
744
757
|
initAudio(disabled).then(() => {
|
|
@@ -759,14 +772,14 @@ function useAudioStatus() {
|
|
|
759
772
|
}
|
|
760
773
|
|
|
761
774
|
// src/ui/app-state.tsx
|
|
762
|
-
import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useState as
|
|
775
|
+
import { createContext as createContext3, useCallback as useCallback2, useContext as useContext3, useState as useState4 } from "react";
|
|
763
776
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
764
777
|
var AppStateContext = createContext3(null);
|
|
765
778
|
function AppStateProvider({
|
|
766
779
|
initialCfg,
|
|
767
780
|
children
|
|
768
781
|
}) {
|
|
769
|
-
const [cfg, setCfgState] =
|
|
782
|
+
const [cfg, setCfgState] = useState4(initialCfg);
|
|
770
783
|
const setCfg = useCallback2(async (next) => {
|
|
771
784
|
setCfgState(next);
|
|
772
785
|
await saveConfig(next);
|
|
@@ -779,14 +792,555 @@ function useAppState() {
|
|
|
779
792
|
return ctx;
|
|
780
793
|
}
|
|
781
794
|
|
|
795
|
+
// src/i18n/context.tsx
|
|
796
|
+
import { createContext as createContext4, useContext as useContext4, useMemo } from "react";
|
|
797
|
+
|
|
798
|
+
// src/i18n/strings.ts
|
|
799
|
+
var en = {
|
|
800
|
+
app: {
|
|
801
|
+
title: "qwerty",
|
|
802
|
+
subtitle: "typing practice for the terminal"
|
|
803
|
+
},
|
|
804
|
+
common: {
|
|
805
|
+
back: "back",
|
|
806
|
+
quit: "quit",
|
|
807
|
+
on: "on",
|
|
808
|
+
off: "off",
|
|
809
|
+
cancel: "cancel"
|
|
810
|
+
},
|
|
811
|
+
mainMenu: {
|
|
812
|
+
items: {
|
|
813
|
+
practiceLabel: "Practice",
|
|
814
|
+
practiceHintWith: (name) => `start ${name}`,
|
|
815
|
+
practiceHintNone: "pick a dictionary",
|
|
816
|
+
dictLabel: "Dictionaries",
|
|
817
|
+
dictHint: "browse, pull, set default",
|
|
818
|
+
wordLabel: "Word lookup",
|
|
819
|
+
wordHint: "search local dicts",
|
|
820
|
+
statsLabel: "Stats",
|
|
821
|
+
statsHint: "history & trends",
|
|
822
|
+
configLabel: "Config",
|
|
823
|
+
configHint: "edit preferences",
|
|
824
|
+
stealthLabel: "Stealth",
|
|
825
|
+
stealthHint: "quiet practice mode",
|
|
826
|
+
quitLabel: "Quit",
|
|
827
|
+
quitHint: "Esc or Ctrl+C also exits"
|
|
828
|
+
},
|
|
829
|
+
hint: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump",
|
|
830
|
+
helpHint: "? help"
|
|
831
|
+
},
|
|
832
|
+
dict: {
|
|
833
|
+
title: "Dictionaries",
|
|
834
|
+
loading: "loading dictionaries\u2026",
|
|
835
|
+
entries: (n) => `${n} entries`,
|
|
836
|
+
filterPlaceholder: "type to filter",
|
|
837
|
+
local: "local \u2713",
|
|
838
|
+
notLocal: "not local",
|
|
839
|
+
defaultMark: "default \u2605",
|
|
840
|
+
tagsLabel: (tags) => `tags: ${tags}`,
|
|
841
|
+
wordsLabel: (n) => `${n} words`,
|
|
842
|
+
pulling: (id) => `pulling ${id}\u2026`,
|
|
843
|
+
removing: (id) => `removing ${id}\u2026`,
|
|
844
|
+
errorOn: (id, msg) => `error on ${id}: ${msg}`,
|
|
845
|
+
footer: "\u2191/\u2193 select \xB7 Enter actions \xB7 Ctrl+K more \xB7 Esc back",
|
|
846
|
+
action: {
|
|
847
|
+
title: "current dictionary",
|
|
848
|
+
setDefault: "set as default",
|
|
849
|
+
practice: "practice now",
|
|
850
|
+
delete: "delete local"
|
|
851
|
+
},
|
|
852
|
+
command: {
|
|
853
|
+
title: "more actions",
|
|
854
|
+
pull: "pull selected",
|
|
855
|
+
import: "import .json",
|
|
856
|
+
refreshList: "update dictionary list"
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
config: {
|
|
860
|
+
title: "Config",
|
|
861
|
+
fields: {
|
|
862
|
+
defaultDict: "default dict",
|
|
863
|
+
defaultMode: "default mode",
|
|
864
|
+
accent: "accent",
|
|
865
|
+
mirror: "dict mirror",
|
|
866
|
+
chapterSize: "chapter size",
|
|
867
|
+
autoplayPronunciation: "autoplay pronunciation",
|
|
868
|
+
soundsMaster: "sounds master",
|
|
869
|
+
soundsKeystroke: "sounds keystroke",
|
|
870
|
+
soundsFeedback: "sounds feedback",
|
|
871
|
+
soundsKeySound: "sounds key sound",
|
|
872
|
+
language: "language",
|
|
873
|
+
stealth: "stealth mode"
|
|
874
|
+
},
|
|
875
|
+
enumValues: {
|
|
876
|
+
stealth: { off: "off", menu: "show in menu", default: "default practice" }
|
|
877
|
+
},
|
|
878
|
+
hints: {
|
|
879
|
+
editing: "type to edit \xB7 Enter save \xB7 Esc cancel",
|
|
880
|
+
bool: "space toggle \xB7 \u2191/\u2193 move \xB7 Esc back",
|
|
881
|
+
enum: "\u2190/\u2192 cycle \xB7 \u2191/\u2193 move \xB7 Esc back",
|
|
882
|
+
dictRef: "Enter pick dict \xB7 \u2191/\u2193 move \xB7 Esc back",
|
|
883
|
+
stringOrInt: "Enter edit \xB7 \u2191/\u2193 move \xB7 Esc back"
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
stats: {
|
|
887
|
+
title: "Stats \xB7 overview",
|
|
888
|
+
loading: "loading stats\u2026",
|
|
889
|
+
none: "No practice history yet.",
|
|
890
|
+
nonePractice: "Run a practice session first.",
|
|
891
|
+
lifetime: "lifetime",
|
|
892
|
+
sessions: "sessions",
|
|
893
|
+
words: "words",
|
|
894
|
+
errors: "errors",
|
|
895
|
+
wpm: "wpm",
|
|
896
|
+
accuracy: "accuracy",
|
|
897
|
+
streak: "streak",
|
|
898
|
+
last: (n) => `last ${n} days (\u2190/\u2192 cycle window)`,
|
|
899
|
+
cycleWindow: "\u2190/\u2192 cycle window \xB7 Esc back",
|
|
900
|
+
recent: "recent sessions",
|
|
901
|
+
topMistakes: "top mistakes",
|
|
902
|
+
footer: "\u2190/\u2192 cycle window \xB7 Esc back",
|
|
903
|
+
maxLabel: "max",
|
|
904
|
+
recentUnits: { words: "w", errors: "err", wpm: "wpm" },
|
|
905
|
+
multiDictSuffix: (n) => ` +${n} more`
|
|
906
|
+
},
|
|
907
|
+
word: {
|
|
908
|
+
title: "Word lookup",
|
|
909
|
+
indexing: "indexing local dictionaries\u2026",
|
|
910
|
+
none: "No local dictionaries.",
|
|
911
|
+
pullFirst: "Pull one in Dictionaries first.",
|
|
912
|
+
countAcross: (n) => `${n} words across local dicts`,
|
|
913
|
+
noMatches: (q) => `no matches for "${q}"`,
|
|
914
|
+
inDict: (name) => `in: ${name}`,
|
|
915
|
+
mistakes: (n, date) => `mistakes: ${n} (last ${date})`,
|
|
916
|
+
footer: "type to filter \xB7 \u2191/\u2193 select \xB7 Esc back"
|
|
917
|
+
},
|
|
918
|
+
practice: {
|
|
919
|
+
loading: "loading\u2026",
|
|
920
|
+
paused: "PAUSED",
|
|
921
|
+
chapterComplete: "CHAPTER COMPLETE",
|
|
922
|
+
chapterLabel: (c, t) => `chapter ${c}/${t}`,
|
|
923
|
+
reviewLabel: "review",
|
|
924
|
+
statusBar: {
|
|
925
|
+
mode: "mode",
|
|
926
|
+
accent: "accent"
|
|
927
|
+
},
|
|
928
|
+
modes: {
|
|
929
|
+
order: "order",
|
|
930
|
+
dictation: "dictation",
|
|
931
|
+
review: "review",
|
|
932
|
+
random: "random",
|
|
933
|
+
loop: "loop"
|
|
934
|
+
},
|
|
935
|
+
accents: {
|
|
936
|
+
us: "us",
|
|
937
|
+
uk: "uk"
|
|
938
|
+
},
|
|
939
|
+
statCards: {
|
|
940
|
+
words: "words",
|
|
941
|
+
errors: "errors",
|
|
942
|
+
wpm: "wpm",
|
|
943
|
+
accuracy: "accuracy",
|
|
944
|
+
elapsed: (t) => `elapsed ${t}`
|
|
945
|
+
},
|
|
946
|
+
pause: {
|
|
947
|
+
title: "PAUSED",
|
|
948
|
+
chapter: (c, t) => `chapter ${c}/${t}`,
|
|
949
|
+
progress: (completed, total) => `${completed}/${total}`,
|
|
950
|
+
hint: "Enter resume \xB7 Esc back to menu"
|
|
951
|
+
},
|
|
952
|
+
summary: {
|
|
953
|
+
loopAgain: "again",
|
|
954
|
+
nextChapter: "next chapter",
|
|
955
|
+
reviewMistakes: "review mistakes",
|
|
956
|
+
backMenu: "back to menu"
|
|
957
|
+
},
|
|
958
|
+
footers: {
|
|
959
|
+
typing: "Ctrl+N skip \xB7 Esc pause \xB7 Tab replay"
|
|
960
|
+
},
|
|
961
|
+
errors: {
|
|
962
|
+
noMistakes: "No mistakes to review yet. Practice some chapters first.",
|
|
963
|
+
dictEmpty: (id) => `Dictionary ${id} is empty.`,
|
|
964
|
+
unknown: "Unknown error"
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
audio: {
|
|
968
|
+
noPlayer: "! No audio player found on PATH (looked for afplay/ffplay/mpg123/paplay/aplay/powershell). Sounds disabled."
|
|
969
|
+
},
|
|
970
|
+
report: {
|
|
971
|
+
title: "Session summary",
|
|
972
|
+
duration: "duration",
|
|
973
|
+
practiced: "practiced",
|
|
974
|
+
chapters: "chapters",
|
|
975
|
+
words: "words",
|
|
976
|
+
accuracy: "accuracy",
|
|
977
|
+
wpm: "wpm",
|
|
978
|
+
newMistakes: "new mistakes",
|
|
979
|
+
farewell: "see you next time.",
|
|
980
|
+
notPracticed: "no practice this run"
|
|
981
|
+
},
|
|
982
|
+
help: {
|
|
983
|
+
title: "Help",
|
|
984
|
+
subtitle: "all shortcuts",
|
|
985
|
+
sections: {
|
|
986
|
+
main: "main menu",
|
|
987
|
+
practice: "practice",
|
|
988
|
+
dict: "dictionaries",
|
|
989
|
+
config: "config",
|
|
990
|
+
stats: "stats",
|
|
991
|
+
word: "word lookup",
|
|
992
|
+
global: "global"
|
|
993
|
+
},
|
|
994
|
+
keys: {
|
|
995
|
+
navigate: "\u2191/\u2193 navigate items",
|
|
996
|
+
select: "Enter confirm / continue",
|
|
997
|
+
letterJump: "letter jump to menu item",
|
|
998
|
+
pause: "Esc pause practice",
|
|
999
|
+
skip: "Ctrl+N skip current word (neutral)",
|
|
1000
|
+
replay: "Tab replay pronunciation",
|
|
1001
|
+
resume: "Enter resume from pause",
|
|
1002
|
+
backMenu: "Esc back to previous screen",
|
|
1003
|
+
backScreen: "Esc close panel or back",
|
|
1004
|
+
nextChapter: "Enter next chapter",
|
|
1005
|
+
reviewMistakes: "m review mistakes",
|
|
1006
|
+
filter: "type to filter list",
|
|
1007
|
+
itemActions: "Enter open actions panel",
|
|
1008
|
+
moreActions: "Ctrl+K more actions panel",
|
|
1009
|
+
cycleWindow: "\u2190/\u2192 cycle day window",
|
|
1010
|
+
stealthToggle: "Ctrl+I toggle stealth info row",
|
|
1011
|
+
helpScreen: "? open this help screen",
|
|
1012
|
+
quit: "Ctrl+C quit immediately"
|
|
1013
|
+
},
|
|
1014
|
+
footer: "Esc back"
|
|
1015
|
+
},
|
|
1016
|
+
stealth: {
|
|
1017
|
+
paused: "paused",
|
|
1018
|
+
chapterDone: "chapter done",
|
|
1019
|
+
resumeHint: "Enter resume \xB7 Esc menu",
|
|
1020
|
+
nextHint: "Enter next \xB7 Esc menu",
|
|
1021
|
+
infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
var zh = {
|
|
1025
|
+
app: {
|
|
1026
|
+
title: "qwerty",
|
|
1027
|
+
subtitle: "\u7EC8\u7AEF\u952E\u76D8\u7EC3\u4E60"
|
|
1028
|
+
},
|
|
1029
|
+
common: {
|
|
1030
|
+
back: "\u8FD4\u56DE",
|
|
1031
|
+
quit: "\u9000\u51FA",
|
|
1032
|
+
on: "\u5F00",
|
|
1033
|
+
off: "\u5173",
|
|
1034
|
+
cancel: "\u53D6\u6D88"
|
|
1035
|
+
},
|
|
1036
|
+
mainMenu: {
|
|
1037
|
+
items: {
|
|
1038
|
+
practiceLabel: "\u7EC3\u4E60",
|
|
1039
|
+
practiceHintWith: (name) => `\u5F00\u59CB ${name}`,
|
|
1040
|
+
practiceHintNone: "\u8BF7\u5148\u9009\u8BCD\u5178",
|
|
1041
|
+
dictLabel: "\u8BCD\u5178",
|
|
1042
|
+
dictHint: "\u6D4F\u89C8\u3001\u4E0B\u8F7D\u3001\u8BBE\u4E3A\u9ED8\u8BA4",
|
|
1043
|
+
wordLabel: "\u67E5\u8BCD",
|
|
1044
|
+
wordHint: "\u5728\u672C\u5730\u8BCD\u5178\u4E2D\u641C\u7D22",
|
|
1045
|
+
statsLabel: "\u7EDF\u8BA1",
|
|
1046
|
+
statsHint: "\u5386\u53F2\u4E0E\u8D8B\u52BF",
|
|
1047
|
+
configLabel: "\u8BBE\u7F6E",
|
|
1048
|
+
configHint: "\u4FEE\u6539\u504F\u597D",
|
|
1049
|
+
stealthLabel: "\u6478\u9C7C",
|
|
1050
|
+
stealthHint: "\u5B89\u9759\u7EC3\u4E60\u6A21\u5F0F",
|
|
1051
|
+
quitLabel: "\u9000\u51FA",
|
|
1052
|
+
quitHint: "Esc \u6216 Ctrl+C \u9000\u51FA"
|
|
1053
|
+
},
|
|
1054
|
+
hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE",
|
|
1055
|
+
helpHint: "? \u5E2E\u52A9"
|
|
1056
|
+
},
|
|
1057
|
+
dict: {
|
|
1058
|
+
title: "\u8BCD\u5178",
|
|
1059
|
+
loading: "\u52A0\u8F7D\u8BCD\u5178\u4E2D\u2026",
|
|
1060
|
+
entries: (n) => `${n} \u90E8\u8BCD\u5178`,
|
|
1061
|
+
filterPlaceholder: "\u8F93\u5165\u8FC7\u6EE4",
|
|
1062
|
+
local: "\u5DF2\u4E0B\u8F7D \u2713",
|
|
1063
|
+
notLocal: "\u672A\u4E0B\u8F7D",
|
|
1064
|
+
defaultMark: "\u9ED8\u8BA4 \u2605",
|
|
1065
|
+
tagsLabel: (tags) => `\u6807\u7B7E:${tags}`,
|
|
1066
|
+
wordsLabel: (n) => `${n} \u8BCD`,
|
|
1067
|
+
pulling: (id) => `\u62C9\u53D6 ${id} \u4E2D\u2026`,
|
|
1068
|
+
removing: (id) => `\u5220\u9664 ${id} \u4E2D\u2026`,
|
|
1069
|
+
errorOn: (id, msg) => `${id} \u51FA\u9519:${msg}`,
|
|
1070
|
+
footer: "\u2191/\u2193 \u9009\u62E9 \xB7 Enter \u64CD\u4F5C \xB7 Ctrl+K \u66F4\u591A \xB7 Esc \u8FD4\u56DE",
|
|
1071
|
+
action: {
|
|
1072
|
+
title: "\u5F53\u524D\u8BCD\u5178",
|
|
1073
|
+
setDefault: "\u8BBE\u4E3A\u9ED8\u8BA4",
|
|
1074
|
+
practice: "\u7ACB\u5373\u7EC3\u4E60",
|
|
1075
|
+
delete: "\u5220\u9664\u672C\u5730"
|
|
1076
|
+
},
|
|
1077
|
+
command: {
|
|
1078
|
+
title: "\u66F4\u591A\u529F\u80FD",
|
|
1079
|
+
pull: "\u62C9\u53D6\u9009\u4E2D",
|
|
1080
|
+
import: "\u5BFC\u5165 .json",
|
|
1081
|
+
refreshList: "\u66F4\u65B0\u8BCD\u5178\u5217\u8868"
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
config: {
|
|
1085
|
+
title: "\u8BBE\u7F6E",
|
|
1086
|
+
fields: {
|
|
1087
|
+
defaultDict: "\u9ED8\u8BA4\u8BCD\u5178",
|
|
1088
|
+
defaultMode: "\u9ED8\u8BA4\u6A21\u5F0F",
|
|
1089
|
+
accent: "\u53D1\u97F3",
|
|
1090
|
+
mirror: "\u8BCD\u5178\u955C\u50CF\u6E90",
|
|
1091
|
+
chapterSize: "\u7AE0\u8282\u5355\u8BCD\u6570",
|
|
1092
|
+
autoplayPronunciation: "\u81EA\u52A8\u64AD\u653E\u53D1\u97F3",
|
|
1093
|
+
soundsMaster: "\u97F3\u6548\u603B\u5F00\u5173",
|
|
1094
|
+
soundsKeystroke: "\u6309\u952E\u97F3",
|
|
1095
|
+
soundsFeedback: "\u53CD\u9988\u97F3",
|
|
1096
|
+
soundsKeySound: "\u6309\u952E\u97F3\u8272",
|
|
1097
|
+
language: "\u8BED\u8A00",
|
|
1098
|
+
stealth: "\u6478\u9C7C\u6A21\u5F0F"
|
|
1099
|
+
},
|
|
1100
|
+
enumValues: {
|
|
1101
|
+
stealth: { off: "\u5173\u95ED", menu: "\u4E3B\u83DC\u5355\u663E\u793A", default: "\u9ED8\u8BA4\u7EC3\u4E60\u6A21\u5F0F" }
|
|
1102
|
+
},
|
|
1103
|
+
hints: {
|
|
1104
|
+
editing: "\u8F93\u5165\u4FEE\u6539 \xB7 Enter \u4FDD\u5B58 \xB7 Esc \u53D6\u6D88",
|
|
1105
|
+
bool: "\u7A7A\u683C\u5207\u6362 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",
|
|
1106
|
+
enum: "\u2190/\u2192 \u5207\u6362 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",
|
|
1107
|
+
dictRef: "Enter \u9009\u8BCD\u5178 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE",
|
|
1108
|
+
stringOrInt: "Enter \u7F16\u8F91 \xB7 \u2191/\u2193 \u79FB\u52A8 \xB7 Esc \u8FD4\u56DE"
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
stats: {
|
|
1112
|
+
title: "\u7EDF\u8BA1 \xB7 \u6982\u89C8",
|
|
1113
|
+
loading: "\u52A0\u8F7D\u7EDF\u8BA1\u4E2D\u2026",
|
|
1114
|
+
none: "\u8FD8\u6CA1\u6709\u7EC3\u4E60\u8BB0\u5F55\u3002",
|
|
1115
|
+
nonePractice: "\u5148\u6765\u4E00\u6B21\u7EC3\u4E60\u5427\u3002",
|
|
1116
|
+
lifetime: "\u7D2F\u8BA1",
|
|
1117
|
+
sessions: "\u4F1A\u8BDD",
|
|
1118
|
+
words: "\u8BCD\u6570",
|
|
1119
|
+
errors: "\u9519\u8BEF",
|
|
1120
|
+
wpm: "\u901F\u5EA6",
|
|
1121
|
+
accuracy: "\u51C6\u786E\u7387",
|
|
1122
|
+
streak: "\u8FDE\u7EED\u5929\u6570",
|
|
1123
|
+
last: (n) => `\u6700\u8FD1 ${n} \u5929 (\u2190/\u2192 \u5207\u6362\u7A97\u53E3)`,
|
|
1124
|
+
cycleWindow: "\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",
|
|
1125
|
+
recent: "\u6700\u8FD1\u4F1A\u8BDD",
|
|
1126
|
+
topMistakes: "\u9AD8\u9891\u9519\u8BCD",
|
|
1127
|
+
footer: "\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",
|
|
1128
|
+
maxLabel: "\u6700\u5927",
|
|
1129
|
+
recentUnits: { words: "\u8BCD", errors: "\u9519", wpm: "\u901F" },
|
|
1130
|
+
multiDictSuffix: (n) => ` \u7B49 ${n} \u90E8`
|
|
1131
|
+
},
|
|
1132
|
+
word: {
|
|
1133
|
+
title: "\u67E5\u8BCD",
|
|
1134
|
+
indexing: "\u7D22\u5F15\u672C\u5730\u8BCD\u5178\u4E2D\u2026",
|
|
1135
|
+
none: "\u6CA1\u6709\u672C\u5730\u8BCD\u5178\u3002",
|
|
1136
|
+
pullFirst: "\u5148\u5728\u300C\u8BCD\u5178\u300D\u4E2D\u62C9\u53D6\u4E00\u90E8\u3002",
|
|
1137
|
+
countAcross: (n) => `\u672C\u5730\u8BCD\u5178\u5171 ${n} \u8BCD`,
|
|
1138
|
+
noMatches: (q) => `\u6CA1\u6709\u5339\u914D\u300C${q}\u300D\u7684\u8BCD`,
|
|
1139
|
+
inDict: (name) => `\u6765\u6E90:${name}`,
|
|
1140
|
+
mistakes: (n, date) => `\u9519\u8FC7 ${n} \u6B21 (\u6700\u8FD1 ${date})`,
|
|
1141
|
+
footer: "\u8F93\u5165\u8FC7\u6EE4 \xB7 \u2191/\u2193 \u9009\u62E9 \xB7 Esc \u8FD4\u56DE"
|
|
1142
|
+
},
|
|
1143
|
+
practice: {
|
|
1144
|
+
loading: "\u52A0\u8F7D\u4E2D\u2026",
|
|
1145
|
+
paused: "\u5DF2\u6682\u505C",
|
|
1146
|
+
chapterComplete: "\u672C\u7AE0\u5B8C\u6210",
|
|
1147
|
+
chapterLabel: (c, t) => `\u7B2C ${c}/${t} \u7AE0`,
|
|
1148
|
+
reviewLabel: "\u590D\u4E60",
|
|
1149
|
+
statusBar: {
|
|
1150
|
+
mode: "\u6A21\u5F0F",
|
|
1151
|
+
accent: "\u53D1\u97F3"
|
|
1152
|
+
},
|
|
1153
|
+
modes: {
|
|
1154
|
+
order: "\u987A\u5E8F",
|
|
1155
|
+
dictation: "\u9ED8\u5199",
|
|
1156
|
+
review: "\u590D\u4E60",
|
|
1157
|
+
random: "\u4E71\u5E8F",
|
|
1158
|
+
loop: "\u5FAA\u73AF"
|
|
1159
|
+
},
|
|
1160
|
+
accents: {
|
|
1161
|
+
us: "\u7F8E",
|
|
1162
|
+
uk: "\u82F1"
|
|
1163
|
+
},
|
|
1164
|
+
statCards: {
|
|
1165
|
+
words: "\u8BCD\u6570",
|
|
1166
|
+
errors: "\u9519\u8BEF",
|
|
1167
|
+
wpm: "\u901F\u5EA6",
|
|
1168
|
+
accuracy: "\u51C6\u786E\u7387",
|
|
1169
|
+
elapsed: (t) => `\u8017\u65F6 ${t}`
|
|
1170
|
+
},
|
|
1171
|
+
pause: {
|
|
1172
|
+
title: "\u5DF2\u6682\u505C",
|
|
1173
|
+
chapter: (c, t) => `\u7B2C ${c}/${t} \u7AE0`,
|
|
1174
|
+
progress: (completed, total) => `${completed}/${total}`,
|
|
1175
|
+
hint: "Enter \u7EE7\u7EED \xB7 Esc \u8FD4\u56DE\u83DC\u5355"
|
|
1176
|
+
},
|
|
1177
|
+
summary: {
|
|
1178
|
+
loopAgain: "\u518D\u6765\u4E00\u904D",
|
|
1179
|
+
nextChapter: "\u4E0B\u4E00\u7AE0",
|
|
1180
|
+
reviewMistakes: "\u590D\u4E60\u9519\u8BCD",
|
|
1181
|
+
backMenu: "\u8FD4\u56DE\u83DC\u5355"
|
|
1182
|
+
},
|
|
1183
|
+
footers: {
|
|
1184
|
+
typing: "Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD"
|
|
1185
|
+
},
|
|
1186
|
+
errors: {
|
|
1187
|
+
noMistakes: "\u9519\u8BCD\u672C\u662F\u7A7A\u7684\u3002\u5148\u7EC3\u4E60\u51E0\u7AE0\u5427\u3002",
|
|
1188
|
+
dictEmpty: (id) => `\u8BCD\u5178 ${id} \u662F\u7A7A\u7684\u3002`,
|
|
1189
|
+
unknown: "\u672A\u77E5\u9519\u8BEF"
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
audio: {
|
|
1193
|
+
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"
|
|
1194
|
+
},
|
|
1195
|
+
report: {
|
|
1196
|
+
title: "\u672C\u6B21\u4F1A\u8BDD",
|
|
1197
|
+
duration: "\u603B\u65F6\u957F",
|
|
1198
|
+
practiced: "\u7EC3\u4E60\u7528\u65F6",
|
|
1199
|
+
chapters: "\u5B8C\u6210\u7AE0\u8282",
|
|
1200
|
+
words: "\u8BCD\u6570",
|
|
1201
|
+
accuracy: "\u51C6\u786E\u7387",
|
|
1202
|
+
wpm: "\u901F\u5EA6",
|
|
1203
|
+
newMistakes: "\u65B0\u9519\u8BCD",
|
|
1204
|
+
farewell: "\u4E0B\u6B21\u89C1\u3002",
|
|
1205
|
+
notPracticed: "\u672C\u6B21\u672A\u7EC3\u4E60"
|
|
1206
|
+
},
|
|
1207
|
+
help: {
|
|
1208
|
+
title: "\u5E2E\u52A9",
|
|
1209
|
+
subtitle: "\u5168\u90E8\u5FEB\u6377\u952E",
|
|
1210
|
+
sections: {
|
|
1211
|
+
main: "\u4E3B\u83DC\u5355",
|
|
1212
|
+
practice: "\u7EC3\u4E60",
|
|
1213
|
+
dict: "\u8BCD\u5178",
|
|
1214
|
+
config: "\u8BBE\u7F6E",
|
|
1215
|
+
stats: "\u7EDF\u8BA1",
|
|
1216
|
+
word: "\u67E5\u8BCD",
|
|
1217
|
+
global: "\u5168\u5C40"
|
|
1218
|
+
},
|
|
1219
|
+
keys: {
|
|
1220
|
+
navigate: "\u2191/\u2193 \u79FB\u52A8\u9009\u9879",
|
|
1221
|
+
select: "Enter \u786E\u8BA4 / \u7EE7\u7EED",
|
|
1222
|
+
letterJump: "\u5B57\u6BCD\u952E \u76F4\u8FBE\u83DC\u5355\u9879",
|
|
1223
|
+
pause: "Esc \u6682\u505C\u7EC3\u4E60",
|
|
1224
|
+
skip: "Ctrl+N \u8DF3\u8FC7\u5F53\u524D\u8BCD(\u4E0D\u8BA1\u9519)",
|
|
1225
|
+
replay: "Tab \u91CD\u64AD\u53D1\u97F3",
|
|
1226
|
+
resume: "Enter \u7EE7\u7EED\u7EC3\u4E60",
|
|
1227
|
+
backMenu: "Esc \u8FD4\u56DE\u4E0A\u4E00\u5C4F",
|
|
1228
|
+
backScreen: "Esc \u5173\u95ED\u9762\u677F / \u8FD4\u56DE",
|
|
1229
|
+
nextChapter: "Enter \u4E0B\u4E00\u7AE0",
|
|
1230
|
+
reviewMistakes: "m \u590D\u4E60\u9519\u8BCD",
|
|
1231
|
+
filter: "\u8F93\u5165 \u8FC7\u6EE4\u5217\u8868",
|
|
1232
|
+
itemActions: "Enter \u5F39\u51FA\u52A8\u4F5C\u9762\u677F",
|
|
1233
|
+
moreActions: "Ctrl+K \u5F39\u51FA\u66F4\u591A\u529F\u80FD",
|
|
1234
|
+
cycleWindow: "\u2190/\u2192 \u5207\u6362\u65E5\u7A97\u53E3",
|
|
1235
|
+
stealthToggle: "Ctrl+I \u5207\u6362\u6478\u9C7C\u4FE1\u606F\u884C",
|
|
1236
|
+
helpScreen: "? \u6253\u5F00\u672C\u5E2E\u52A9\u9875",
|
|
1237
|
+
quit: "Ctrl+C \u7ACB\u5373\u9000\u51FA"
|
|
1238
|
+
},
|
|
1239
|
+
footer: "Esc \u8FD4\u56DE"
|
|
1240
|
+
},
|
|
1241
|
+
stealth: {
|
|
1242
|
+
paused: "paused",
|
|
1243
|
+
chapterDone: "chapter done",
|
|
1244
|
+
resumeHint: "Enter resume \xB7 Esc menu",
|
|
1245
|
+
nextHint: "Enter next \xB7 Esc menu",
|
|
1246
|
+
infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// src/i18n/locale.ts
|
|
1251
|
+
function pickFromString(s) {
|
|
1252
|
+
if (!s) return null;
|
|
1253
|
+
const lower = s.toLowerCase();
|
|
1254
|
+
if (lower.startsWith("zh")) return "zh";
|
|
1255
|
+
if (lower.startsWith("en")) return "en";
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
function detectLocale(pref) {
|
|
1259
|
+
if (pref === "zh" || pref === "en") return pref;
|
|
1260
|
+
const env = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE;
|
|
1261
|
+
const fromEnv = pickFromString(env);
|
|
1262
|
+
if (fromEnv) return fromEnv;
|
|
1263
|
+
try {
|
|
1264
|
+
const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1265
|
+
const fromIntl = pickFromString(intlLocale);
|
|
1266
|
+
if (fromIntl) return fromIntl;
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
return "en";
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/i18n/context.tsx
|
|
1273
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1274
|
+
var StringsContext = createContext4(null);
|
|
1275
|
+
function StringsProvider({
|
|
1276
|
+
pref,
|
|
1277
|
+
children
|
|
1278
|
+
}) {
|
|
1279
|
+
const value = useMemo(() => {
|
|
1280
|
+
const lang = detectLocale(pref);
|
|
1281
|
+
return { lang, t: lang === "zh" ? zh : en };
|
|
1282
|
+
}, [pref]);
|
|
1283
|
+
return /* @__PURE__ */ jsx5(StringsContext.Provider, { value, children });
|
|
1284
|
+
}
|
|
1285
|
+
function useStrings() {
|
|
1286
|
+
const ctx = useContext4(StringsContext);
|
|
1287
|
+
if (!ctx) throw new Error("useStrings must be used inside StringsProvider");
|
|
1288
|
+
return ctx.t;
|
|
1289
|
+
}
|
|
1290
|
+
function pickStrings(pref) {
|
|
1291
|
+
const lang = detectLocale(pref);
|
|
1292
|
+
return { lang, t: lang === "zh" ? zh : en };
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/ui/registry-context.tsx
|
|
1296
|
+
import { createContext as createContext5, useContext as useContext5, useEffect as useEffect3, useState as useState5 } from "react";
|
|
1297
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1298
|
+
var RegistryContext = createContext5(null);
|
|
1299
|
+
function RegistryProvider({ children }) {
|
|
1300
|
+
const [registry, setRegistry] = useState5(null);
|
|
1301
|
+
const [byId, setById] = useState5(/* @__PURE__ */ new Map());
|
|
1302
|
+
useEffect3(() => {
|
|
1303
|
+
let cancelled = false;
|
|
1304
|
+
(async () => {
|
|
1305
|
+
try {
|
|
1306
|
+
const reg = await loadRegistry();
|
|
1307
|
+
if (cancelled) return;
|
|
1308
|
+
const map = /* @__PURE__ */ new Map();
|
|
1309
|
+
for (const e of reg) map.set(e.id, e);
|
|
1310
|
+
setRegistry(reg);
|
|
1311
|
+
setById(map);
|
|
1312
|
+
} catch {
|
|
1313
|
+
if (!cancelled) {
|
|
1314
|
+
setRegistry([]);
|
|
1315
|
+
setById(/* @__PURE__ */ new Map());
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
})();
|
|
1319
|
+
return () => {
|
|
1320
|
+
cancelled = true;
|
|
1321
|
+
};
|
|
1322
|
+
}, []);
|
|
1323
|
+
return /* @__PURE__ */ jsx6(RegistryContext.Provider, { value: { registry, byId }, children });
|
|
1324
|
+
}
|
|
1325
|
+
function useRegistry() {
|
|
1326
|
+
const ctx = useContext5(RegistryContext);
|
|
1327
|
+
if (!ctx) throw new Error("useRegistry must be used inside RegistryProvider");
|
|
1328
|
+
return ctx;
|
|
1329
|
+
}
|
|
1330
|
+
function useDictName(id) {
|
|
1331
|
+
const { byId } = useRegistry();
|
|
1332
|
+
if (!id) return "";
|
|
1333
|
+
const entry = byId.get(id);
|
|
1334
|
+
return entry?.name ?? id;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
782
1337
|
// src/ui/screens/MainMenu.tsx
|
|
783
|
-
import { useState as
|
|
784
|
-
import { Box as
|
|
1338
|
+
import { useState as useState6 } from "react";
|
|
1339
|
+
import { Box as Box3, Text as Text2, useApp, useInput } from "ink";
|
|
785
1340
|
|
|
786
1341
|
// src/ui/components/BigWord.tsx
|
|
787
|
-
import { Box, Text, useStdout } from "ink";
|
|
788
|
-
import
|
|
789
|
-
import { jsx as jsx5, jsxs } from "react/jsx-runtime";
|
|
1342
|
+
import { Box as Box2, Text, useStdout as useStdout2 } from "ink";
|
|
1343
|
+
import { jsx as jsx7, jsxs } from "react/jsx-runtime";
|
|
790
1344
|
var PALETTE = {
|
|
791
1345
|
accent: "#5eead4",
|
|
792
1346
|
muted: "#6b7280",
|
|
@@ -797,66 +1351,127 @@ var PALETTE = {
|
|
|
797
1351
|
error: "#f87171"
|
|
798
1352
|
};
|
|
799
1353
|
function BigWord({ target, typed, error = false, hideTarget = false }) {
|
|
800
|
-
const { stdout } =
|
|
1354
|
+
const { stdout } = useStdout2();
|
|
801
1355
|
const cols = stdout?.columns ?? 80;
|
|
802
1356
|
const chars = [...target];
|
|
803
1357
|
const typedChars = [...typed];
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
1358
|
+
const sep = cols >= 80 ? " " : cols >= 60 ? " " : " ";
|
|
1359
|
+
return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", alignItems: "center", paddingY: 1, children: [
|
|
1360
|
+
/* @__PURE__ */ jsx7(Box2, { children: chars.map((ch, i) => {
|
|
807
1361
|
const isTyped = i < typedChars.length;
|
|
808
1362
|
const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
|
|
809
|
-
const color =
|
|
810
|
-
return /* @__PURE__ */ jsxs(Text, { bold:
|
|
1363
|
+
const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;
|
|
1364
|
+
return /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
|
|
811
1365
|
display,
|
|
812
|
-
|
|
1366
|
+
i < chars.length - 1 ? sep : ""
|
|
813
1367
|
] }, i);
|
|
814
|
-
}) })
|
|
1368
|
+
}) }),
|
|
1369
|
+
/* @__PURE__ */ jsx7(Box2, { children: chars.map((ch, i) => {
|
|
1370
|
+
const isTyped = i < typedChars.length;
|
|
1371
|
+
const trackChar = isTyped ? "\u2501" : "\u2500";
|
|
1372
|
+
const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;
|
|
1373
|
+
return /* @__PURE__ */ jsxs(Text, { color, children: [
|
|
1374
|
+
trackChar,
|
|
1375
|
+
i < chars.length - 1 ? sep : ""
|
|
1376
|
+
] }, i);
|
|
1377
|
+
}) })
|
|
1378
|
+
] });
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// src/util/text.ts
|
|
1382
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
1383
|
+
function stripAnsi(s) {
|
|
1384
|
+
return s.replace(ANSI_RE, "");
|
|
1385
|
+
}
|
|
1386
|
+
function visibleWidth2(s) {
|
|
1387
|
+
const plain = stripAnsi(s);
|
|
1388
|
+
let w = 0;
|
|
1389
|
+
for (const ch of plain) {
|
|
1390
|
+
const code = ch.codePointAt(0);
|
|
1391
|
+
w += code > 11904 && code < 64256 ? 2 : 1;
|
|
815
1392
|
}
|
|
816
|
-
return
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1393
|
+
return w;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/util/dict-name.ts
|
|
1397
|
+
function truncateName(name, max) {
|
|
1398
|
+
if (visibleWidth2(name) <= max) return name;
|
|
1399
|
+
let out = "";
|
|
1400
|
+
let w = 0;
|
|
1401
|
+
for (const ch of name) {
|
|
1402
|
+
const code = ch.codePointAt(0);
|
|
1403
|
+
const cw = code > 11904 && code < 64256 ? 2 : 1;
|
|
1404
|
+
if (w + cw > max - 1) break;
|
|
1405
|
+
out += ch;
|
|
1406
|
+
w += cw;
|
|
1407
|
+
}
|
|
1408
|
+
return out + "\u2026";
|
|
822
1409
|
}
|
|
823
1410
|
|
|
824
1411
|
// src/ui/screens/MainMenu.tsx
|
|
825
|
-
import { jsx as
|
|
1412
|
+
import { jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
826
1413
|
function MainMenu({ cfg }) {
|
|
827
|
-
const [selected, setSelected] =
|
|
1414
|
+
const [selected, setSelected] = useState6(0);
|
|
828
1415
|
const { exit } = useApp();
|
|
829
1416
|
const nav = useNav();
|
|
830
1417
|
const audio = useAudioStatus();
|
|
1418
|
+
const t = useStrings();
|
|
1419
|
+
const defaultDictName = useDictName(cfg.defaultDict);
|
|
1420
|
+
const m = t.mainMenu.items;
|
|
1421
|
+
const startPractice = (stealth) => {
|
|
1422
|
+
if (cfg.defaultDict) {
|
|
1423
|
+
nav.navigate({
|
|
1424
|
+
name: "practice",
|
|
1425
|
+
params: {
|
|
1426
|
+
dictId: cfg.defaultDict,
|
|
1427
|
+
chapterIndex: 0,
|
|
1428
|
+
mode: cfg.defaultMode,
|
|
1429
|
+
stealth
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
} else {
|
|
1433
|
+
nav.navigate({ name: "dict", params: { pickerMode: "choose-then-practice" } });
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
831
1436
|
const items = [
|
|
832
1437
|
{
|
|
833
1438
|
key: "p",
|
|
834
|
-
label:
|
|
835
|
-
hint: cfg.defaultDict ?
|
|
836
|
-
run: () =>
|
|
837
|
-
|
|
838
|
-
nav.navigate({
|
|
839
|
-
name: "practice",
|
|
840
|
-
params: { dictId: cfg.defaultDict, chapterIndex: 0, mode: cfg.defaultMode }
|
|
841
|
-
});
|
|
842
|
-
} else {
|
|
843
|
-
nav.navigate({ name: "dict", params: { pickerMode: "choose-then-practice" } });
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
},
|
|
847
|
-
{ key: "d", label: "Dictionaries", hint: "browse, pull, set default", run: () => nav.navigate({ name: "dict" }) },
|
|
848
|
-
{ key: "w", label: "Word lookup", hint: "search local dicts", run: () => nav.navigate({ name: "word" }) },
|
|
849
|
-
{ key: "s", label: "Stats", hint: "history & trends", run: () => nav.navigate({ name: "stats" }) },
|
|
850
|
-
{ key: "c", label: "Config", hint: "edit preferences", run: () => nav.navigate({ name: "config" }) },
|
|
851
|
-
{ key: "q", label: "Quit", hint: "Ctrl+C also exits", run: () => exit() }
|
|
1439
|
+
label: m.practiceLabel,
|
|
1440
|
+
hint: cfg.defaultDict ? m.practiceHintWith(truncateName(defaultDictName, 24)) : m.practiceHintNone,
|
|
1441
|
+
run: () => startPractice(cfg.stealth === "default")
|
|
1442
|
+
}
|
|
852
1443
|
];
|
|
1444
|
+
if (cfg.stealth === "menu" || cfg.stealth === "default") {
|
|
1445
|
+
items.push({
|
|
1446
|
+
key: "b",
|
|
1447
|
+
label: m.stealthLabel,
|
|
1448
|
+
hint: m.stealthHint,
|
|
1449
|
+
run: () => startPractice(true)
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
items.push(
|
|
1453
|
+
{ key: "d", label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: "dict" }) },
|
|
1454
|
+
{ key: "w", label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: "word" }) },
|
|
1455
|
+
{ key: "s", label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: "stats" }) },
|
|
1456
|
+
{ key: "c", label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: "config" }) },
|
|
1457
|
+
{ key: "q", label: m.quitLabel, hint: m.quitHint, run: () => exit() }
|
|
1458
|
+
);
|
|
1459
|
+
const labelW = Math.max(...items.map((it) => visibleWidth2(it.label))) + 4;
|
|
853
1460
|
useInput((input, key) => {
|
|
1461
|
+
if (key.escape) {
|
|
1462
|
+
exit();
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
854
1465
|
if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);
|
|
855
1466
|
if (key.downArrow) setSelected((i) => (i + 1) % items.length);
|
|
856
1467
|
if (key.return) {
|
|
857
1468
|
items[selected].run();
|
|
858
1469
|
return;
|
|
859
1470
|
}
|
|
1471
|
+
if (input === "?") {
|
|
1472
|
+
nav.navigate({ name: "help" });
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
860
1475
|
for (const it of items) {
|
|
861
1476
|
if (input === it.key) {
|
|
862
1477
|
it.run();
|
|
@@ -864,41 +1479,44 @@ function MainMenu({ cfg }) {
|
|
|
864
1479
|
}
|
|
865
1480
|
}
|
|
866
1481
|
});
|
|
867
|
-
return /* @__PURE__ */ jsxs2(
|
|
868
|
-
/* @__PURE__ */ jsxs2(
|
|
869
|
-
/* @__PURE__ */
|
|
870
|
-
/* @__PURE__ */
|
|
1482
|
+
return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", children: [
|
|
1483
|
+
/* @__PURE__ */ jsxs2(Box3, { children: [
|
|
1484
|
+
/* @__PURE__ */ jsx8(Text2, { bold: true, color: PALETTE.accent, children: t.app.title }),
|
|
1485
|
+
/* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
|
|
1486
|
+
" \xB7 ",
|
|
1487
|
+
t.app.subtitle
|
|
1488
|
+
] })
|
|
871
1489
|
] }),
|
|
872
|
-
/* @__PURE__ */
|
|
1490
|
+
/* @__PURE__ */ jsx8(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
|
|
873
1491
|
const active = i === selected;
|
|
874
|
-
|
|
875
|
-
|
|
1492
|
+
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(it.label)));
|
|
1493
|
+
return /* @__PURE__ */ jsxs2(Box3, { children: [
|
|
1494
|
+
/* @__PURE__ */ jsx8(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
876
1495
|
/* @__PURE__ */ jsxs2(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
|
|
877
1496
|
"[",
|
|
878
1497
|
it.key,
|
|
879
1498
|
"]"
|
|
880
1499
|
] }),
|
|
881
|
-
/* @__PURE__ */
|
|
882
|
-
/* @__PURE__ */
|
|
883
|
-
|
|
1500
|
+
/* @__PURE__ */ jsx8(Text2, { children: " " }),
|
|
1501
|
+
/* @__PURE__ */ jsxs2(Text2, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
|
|
1502
|
+
it.label,
|
|
1503
|
+
pad
|
|
1504
|
+
] }),
|
|
1505
|
+
/* @__PURE__ */ jsx8(Text2, { color: PALETTE.muted, children: it.hint })
|
|
884
1506
|
] }, it.key);
|
|
885
1507
|
}) }),
|
|
886
|
-
/* @__PURE__ */
|
|
887
|
-
|
|
888
|
-
|
|
1508
|
+
/* @__PURE__ */ jsx8(Box3, { marginTop: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
|
|
1509
|
+
t.mainMenu.hint,
|
|
1510
|
+
" \xB7 ",
|
|
1511
|
+
t.mainMenu.helpHint
|
|
889
1512
|
] }) }),
|
|
890
|
-
/* @__PURE__ */
|
|
891
|
-
audio.warning && /* @__PURE__ */ jsx6(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.warning, children: [
|
|
892
|
-
"! ",
|
|
893
|
-
audio.warning
|
|
894
|
-
] }) })
|
|
1513
|
+
audio.warning && /* @__PURE__ */ jsx8(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text2, { color: PALETTE.warning, children: t.audio.noPlayer }) })
|
|
895
1514
|
] });
|
|
896
1515
|
}
|
|
897
1516
|
|
|
898
1517
|
// src/ui/screens/PracticeScreen.tsx
|
|
899
|
-
import { useState as
|
|
900
|
-
import { Box as
|
|
901
|
-
import BigText2 from "ink-big-text";
|
|
1518
|
+
import { useState as useState8, useEffect as useEffect6, useRef as useRef3 } from "react";
|
|
1519
|
+
import { Box as Box5, Text as Text4, useApp as useApp3, useInput as useInput3 } from "ink";
|
|
902
1520
|
|
|
903
1521
|
// src/util/shuffle.ts
|
|
904
1522
|
function shuffle(arr, rng = Math.random) {
|
|
@@ -942,25 +1560,25 @@ function buildPlaylist(chapter, mode, seed) {
|
|
|
942
1560
|
function initialState(target) {
|
|
943
1561
|
return { target, typed: "", errorsThisWord: 0 };
|
|
944
1562
|
}
|
|
945
|
-
function reduce(
|
|
1563
|
+
function reduce(state2, ev) {
|
|
946
1564
|
switch (ev.type) {
|
|
947
1565
|
case "reset":
|
|
948
|
-
return { state: { ...
|
|
1566
|
+
return { state: { ...state2, typed: "" }, effect: "none" };
|
|
949
1567
|
case "backspace": {
|
|
950
|
-
if (
|
|
951
|
-
return { state: { ...
|
|
1568
|
+
if (state2.typed.length === 0) return { state: state2, effect: "none" };
|
|
1569
|
+
return { state: { ...state2, typed: state2.typed.slice(0, -1) }, effect: "none" };
|
|
952
1570
|
}
|
|
953
1571
|
case "char": {
|
|
954
|
-
const candidate =
|
|
955
|
-
const targetUpToCandidate = [...
|
|
1572
|
+
const candidate = state2.typed + ev.ch;
|
|
1573
|
+
const targetUpToCandidate = [...state2.target].slice(0, [...candidate].length).join("");
|
|
956
1574
|
if (candidate === targetUpToCandidate) {
|
|
957
|
-
if (candidate.length ===
|
|
958
|
-
return { state: { ...
|
|
1575
|
+
if (candidate.length === state2.target.length) {
|
|
1576
|
+
return { state: { ...state2, typed: candidate }, effect: "correct" };
|
|
959
1577
|
}
|
|
960
|
-
return { state: { ...
|
|
1578
|
+
return { state: { ...state2, typed: candidate }, effect: "progress" };
|
|
961
1579
|
}
|
|
962
1580
|
return {
|
|
963
|
-
state: { ...
|
|
1581
|
+
state: { ...state2, typed: "", errorsThisWord: state2.errorsThisWord + 1 },
|
|
964
1582
|
effect: "wrong"
|
|
965
1583
|
};
|
|
966
1584
|
}
|
|
@@ -982,11 +1600,11 @@ function startSession(playlist, now = Date.now()) {
|
|
|
982
1600
|
}
|
|
983
1601
|
function feedSession(session, ev, now = Date.now()) {
|
|
984
1602
|
if (!session.current) return { session, effect: "none" };
|
|
985
|
-
const { state, effect } = reduce(session.current.input, ev);
|
|
1603
|
+
const { state: state2, effect } = reduce(session.current.input, ev);
|
|
986
1604
|
if (effect === "correct") {
|
|
987
1605
|
const finished = {
|
|
988
|
-
word:
|
|
989
|
-
errors:
|
|
1606
|
+
word: state2.target,
|
|
1607
|
+
errors: state2.errorsThisWord,
|
|
990
1608
|
durationMs: now - session.current.wordStartedAt
|
|
991
1609
|
};
|
|
992
1610
|
const nextIndex = session.current.wordIndex + 1;
|
|
@@ -1013,7 +1631,7 @@ function feedSession(session, ev, now = Date.now()) {
|
|
|
1013
1631
|
return {
|
|
1014
1632
|
session: {
|
|
1015
1633
|
...session,
|
|
1016
|
-
current: { ...session.current, input:
|
|
1634
|
+
current: { ...session.current, input: state2 }
|
|
1017
1635
|
},
|
|
1018
1636
|
effect
|
|
1019
1637
|
};
|
|
@@ -1097,24 +1715,24 @@ function topN(book, n) {
|
|
|
1097
1715
|
}
|
|
1098
1716
|
|
|
1099
1717
|
// src/ui/hooks/useWordLoop.ts
|
|
1100
|
-
import { useEffect as
|
|
1718
|
+
import { useEffect as useEffect4, useReducer, useRef, useState as useState7 } from "react";
|
|
1101
1719
|
import { useInput as useInput2, useApp as useApp2 } from "ink";
|
|
1102
|
-
function reducer(
|
|
1720
|
+
function reducer(state2, action) {
|
|
1103
1721
|
if (action.type === "start") {
|
|
1104
1722
|
return { session: startSession(action.playlist, action.now), lastEffect: null };
|
|
1105
1723
|
}
|
|
1106
1724
|
if (action.type === "skip") {
|
|
1107
|
-
const r = skipSession(
|
|
1725
|
+
const r = skipSession(state2.session, action.now);
|
|
1108
1726
|
return { session: r.session, lastEffect: r.effect };
|
|
1109
1727
|
}
|
|
1110
1728
|
if (action.type === "event") {
|
|
1111
1729
|
if (action.key.backspace || action.key.delete) {
|
|
1112
|
-
const r = feedSession(
|
|
1730
|
+
const r = feedSession(state2.session, { type: "backspace" }, action.now);
|
|
1113
1731
|
return { session: r.session, lastEffect: r.effect };
|
|
1114
1732
|
}
|
|
1115
|
-
if (action.input.length === 0) return
|
|
1116
|
-
let session =
|
|
1117
|
-
let lastEffect =
|
|
1733
|
+
if (action.input.length === 0) return state2;
|
|
1734
|
+
let session = state2.session;
|
|
1735
|
+
let lastEffect = state2.lastEffect;
|
|
1118
1736
|
for (const c of action.input) {
|
|
1119
1737
|
const r = feedSession(session, { type: "char", ch: c }, action.now);
|
|
1120
1738
|
session = r.session;
|
|
@@ -1123,15 +1741,15 @@ function reducer(state, action) {
|
|
|
1123
1741
|
}
|
|
1124
1742
|
return { session, lastEffect };
|
|
1125
1743
|
}
|
|
1126
|
-
return
|
|
1744
|
+
return state2;
|
|
1127
1745
|
}
|
|
1128
1746
|
function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled = true }) {
|
|
1129
|
-
const [
|
|
1747
|
+
const [state2, dispatch] = useReducer(reducer, void 0, () => ({
|
|
1130
1748
|
session: startSession(playlist, Date.now()),
|
|
1131
1749
|
lastEffect: null
|
|
1132
1750
|
}));
|
|
1133
1751
|
const completedRef = useRef(false);
|
|
1134
|
-
const [tick, setTick] =
|
|
1752
|
+
const [tick, setTick] = useState7(0);
|
|
1135
1753
|
const { exit } = useApp2();
|
|
1136
1754
|
useInput2(
|
|
1137
1755
|
(input, key) => {
|
|
@@ -1163,25 +1781,25 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1163
1781
|
},
|
|
1164
1782
|
{ isActive: enabled }
|
|
1165
1783
|
);
|
|
1166
|
-
|
|
1167
|
-
if (
|
|
1784
|
+
useEffect4(() => {
|
|
1785
|
+
if (state2.session.finishedAt !== null && !completedRef.current) {
|
|
1168
1786
|
completedRef.current = true;
|
|
1169
|
-
onComplete(
|
|
1787
|
+
onComplete(state2.session);
|
|
1170
1788
|
}
|
|
1171
|
-
}, [
|
|
1172
|
-
|
|
1173
|
-
if (
|
|
1789
|
+
}, [state2.session, onComplete]);
|
|
1790
|
+
useEffect4(() => {
|
|
1791
|
+
if (state2.session.finishedAt !== null) return;
|
|
1174
1792
|
const id = setInterval(() => setTick((t) => t + 1), 1e3);
|
|
1175
1793
|
return () => clearInterval(id);
|
|
1176
|
-
}, [
|
|
1177
|
-
return { session:
|
|
1794
|
+
}, [state2.session.finishedAt]);
|
|
1795
|
+
return { session: state2.session, lastEffect: state2.lastEffect, tick };
|
|
1178
1796
|
}
|
|
1179
1797
|
|
|
1180
1798
|
// src/ui/hooks/useAudio.ts
|
|
1181
|
-
import { useEffect as
|
|
1799
|
+
import { useEffect as useEffect5, useRef as useRef2 } from "react";
|
|
1182
1800
|
function useAudio(opts) {
|
|
1183
1801
|
const initedRef = useRef2(false);
|
|
1184
|
-
|
|
1802
|
+
useEffect5(() => {
|
|
1185
1803
|
if (initedRef.current) return;
|
|
1186
1804
|
initedRef.current = true;
|
|
1187
1805
|
initAudio(!opts.enabled).catch(() => void 0);
|
|
@@ -1284,6 +1902,43 @@ function dailyBuckets(sessions, days, now = /* @__PURE__ */ new Date()) {
|
|
|
1284
1902
|
return out;
|
|
1285
1903
|
}
|
|
1286
1904
|
|
|
1905
|
+
// src/infra/session-tracker.ts
|
|
1906
|
+
var state = {
|
|
1907
|
+
startedAt: null,
|
|
1908
|
+
chapters: []
|
|
1909
|
+
};
|
|
1910
|
+
function start(now = Date.now()) {
|
|
1911
|
+
if (state.startedAt === null) state.startedAt = now;
|
|
1912
|
+
}
|
|
1913
|
+
function addChapter(entry) {
|
|
1914
|
+
if (state.startedAt === null) state.startedAt = Date.now();
|
|
1915
|
+
state.chapters.push(entry);
|
|
1916
|
+
}
|
|
1917
|
+
function report(now = Date.now()) {
|
|
1918
|
+
const chapters = state.chapters;
|
|
1919
|
+
const wordCount = chapters.reduce((a, c) => a + c.wordCount, 0);
|
|
1920
|
+
const errors = chapters.reduce((a, c) => a + c.errors, 0);
|
|
1921
|
+
const practiceMs = chapters.reduce((a, c) => a + c.durationMs, 0);
|
|
1922
|
+
const minutes = practiceMs / 6e4;
|
|
1923
|
+
const wpm = minutes > 0 ? Math.round(wordCount / minutes * 10) / 10 : 0;
|
|
1924
|
+
const errorWordSet = /* @__PURE__ */ new Set();
|
|
1925
|
+
for (const c of chapters) {
|
|
1926
|
+
for (const w of Object.keys(c.perWordErrors)) errorWordSet.add(w);
|
|
1927
|
+
}
|
|
1928
|
+
const accuracy2 = wordCount === 0 ? 1 : Math.max(0, (wordCount - errorWordSet.size) / wordCount);
|
|
1929
|
+
return {
|
|
1930
|
+
startedAt: state.startedAt,
|
|
1931
|
+
totalDurationMs: state.startedAt === null ? 0 : now - state.startedAt,
|
|
1932
|
+
chaptersCompleted: chapters.length,
|
|
1933
|
+
wordCount,
|
|
1934
|
+
errors,
|
|
1935
|
+
wpm,
|
|
1936
|
+
accuracy: accuracy2,
|
|
1937
|
+
newMistakeWords: errorWordSet.size,
|
|
1938
|
+
practiceMs
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1287
1942
|
// src/ui/hooks/useSessionPersistence.ts
|
|
1288
1943
|
function useSessionPersistence(meta) {
|
|
1289
1944
|
return useCallback3(
|
|
@@ -1299,6 +1954,15 @@ function useSessionPersistence(meta) {
|
|
|
1299
1954
|
perWordErrors: summary.perWordErrors
|
|
1300
1955
|
};
|
|
1301
1956
|
await appendSession(rec);
|
|
1957
|
+
addChapter({
|
|
1958
|
+
dictId: meta.dictId,
|
|
1959
|
+
chapterIndex: meta.chapterIndex,
|
|
1960
|
+
mode: meta.mode,
|
|
1961
|
+
wordCount: summary.wordCount,
|
|
1962
|
+
errors: summary.errors,
|
|
1963
|
+
durationMs: summary.durationMs,
|
|
1964
|
+
perWordErrors: summary.perWordErrors
|
|
1965
|
+
});
|
|
1302
1966
|
const dirty = Object.entries(summary.perWordErrors).filter(([, n]) => n > 0);
|
|
1303
1967
|
if (dirty.length === 0) return;
|
|
1304
1968
|
let book = await loadMistakes();
|
|
@@ -1309,16 +1973,80 @@ function useSessionPersistence(meta) {
|
|
|
1309
1973
|
);
|
|
1310
1974
|
}
|
|
1311
1975
|
|
|
1976
|
+
// src/ui/screens/StealthPracticeLayout.tsx
|
|
1977
|
+
import { Box as Box4, Text as Text3 } from "ink";
|
|
1978
|
+
import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1979
|
+
var TYPED = "#d4d4d4";
|
|
1980
|
+
var UNTYPED = "#808080";
|
|
1981
|
+
var DIM = "#6b6b6b";
|
|
1982
|
+
function fmtTime(ms) {
|
|
1983
|
+
const total = Math.floor(ms / 1e3);
|
|
1984
|
+
const m = Math.floor(total / 60);
|
|
1985
|
+
const s = total % 60;
|
|
1986
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
1987
|
+
}
|
|
1988
|
+
function StealthTyping(props) {
|
|
1989
|
+
const t = useStrings();
|
|
1990
|
+
const target = [...props.target];
|
|
1991
|
+
const typed = [...props.typed];
|
|
1992
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: "100%", height: "100%", paddingX: 4, paddingY: 3, children: [
|
|
1993
|
+
/* @__PURE__ */ jsx9(Box4, { flexGrow: 1 }),
|
|
1994
|
+
/* @__PURE__ */ jsxs3(Box4, { children: [
|
|
1995
|
+
/* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "[" }),
|
|
1996
|
+
target.map((ch, i) => {
|
|
1997
|
+
const isTyped = i < typed.length;
|
|
1998
|
+
const display = props.hideTarget && !isTyped ? "_" : isTyped ? typed[i] : ch;
|
|
1999
|
+
const color = isTyped ? TYPED : UNTYPED;
|
|
2000
|
+
return /* @__PURE__ */ jsx9(Text3, { color, inverse: props.error && isTyped && i === typed.length - 1, children: display }, i);
|
|
2001
|
+
}),
|
|
2002
|
+
/* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "]" })
|
|
2003
|
+
] }),
|
|
2004
|
+
/* @__PURE__ */ jsxs3(Box4, { children: [
|
|
2005
|
+
props.phonetic && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.phonetic }),
|
|
2006
|
+
props.phonetic && props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: " \xB7 " }),
|
|
2007
|
+
props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.translation.slice(0, 1).join("") })
|
|
2008
|
+
] }),
|
|
2009
|
+
/* @__PURE__ */ jsx9(Box4, { children: props.info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.infoFmt(
|
|
2010
|
+
props.info.dictName,
|
|
2011
|
+
props.info.chapterLabel,
|
|
2012
|
+
props.info.completed,
|
|
2013
|
+
props.info.total,
|
|
2014
|
+
props.info.wpm,
|
|
2015
|
+
props.info.accPct
|
|
2016
|
+
) }) : /* @__PURE__ */ jsx9(Text3, { children: " " }) }),
|
|
2017
|
+
/* @__PURE__ */ jsx9(Box4, { flexGrow: 3 })
|
|
2018
|
+
] });
|
|
2019
|
+
}
|
|
2020
|
+
function StealthPaused() {
|
|
2021
|
+
const t = useStrings();
|
|
2022
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: "100%", height: "100%", paddingX: 4, paddingY: 3, children: [
|
|
2023
|
+
/* @__PURE__ */ jsx9(Box4, { flexGrow: 1 }),
|
|
2024
|
+
/* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: t.stealth.paused }) }),
|
|
2025
|
+
/* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.resumeHint }) }),
|
|
2026
|
+
/* @__PURE__ */ jsx9(Box4, { flexGrow: 3 })
|
|
2027
|
+
] });
|
|
2028
|
+
}
|
|
2029
|
+
function StealthSummary(props) {
|
|
2030
|
+
const t = useStrings();
|
|
2031
|
+
const line = `${t.stealth.chapterDone} \xB7 ${props.wordCount}w \xB7 ${props.wpm}wpm \xB7 ${props.accPct}% \xB7 ${fmtTime(props.durationMs)}`;
|
|
2032
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", width: "100%", height: "100%", paddingX: 4, paddingY: 3, children: [
|
|
2033
|
+
/* @__PURE__ */ jsx9(Box4, { flexGrow: 1 }),
|
|
2034
|
+
/* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: line }) }),
|
|
2035
|
+
/* @__PURE__ */ jsx9(Box4, { children: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.nextHint }) }),
|
|
2036
|
+
/* @__PURE__ */ jsx9(Box4, { flexGrow: 3 })
|
|
2037
|
+
] });
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1312
2040
|
// src/ui/screens/PracticeScreen.tsx
|
|
1313
|
-
import { jsx as
|
|
2041
|
+
import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1314
2042
|
function PracticeScreen({ params }) {
|
|
1315
2043
|
const { dictId, chapterIndex, mode } = params;
|
|
1316
2044
|
const { cfg } = useAppState();
|
|
1317
|
-
const
|
|
1318
|
-
const [phase, setPhase] =
|
|
1319
|
-
const [loaded, setLoaded] =
|
|
1320
|
-
const [errorMsg, setErrorMsg] =
|
|
1321
|
-
|
|
2045
|
+
const t = useStrings();
|
|
2046
|
+
const [phase, setPhase] = useState8("loading");
|
|
2047
|
+
const [loaded, setLoaded] = useState8(null);
|
|
2048
|
+
const [errorMsg, setErrorMsg] = useState8(null);
|
|
2049
|
+
useEffect6(() => {
|
|
1322
2050
|
let cancelled = false;
|
|
1323
2051
|
setPhase("loading");
|
|
1324
2052
|
setLoaded(null);
|
|
@@ -1332,7 +2060,7 @@ function PracticeScreen({ params }) {
|
|
|
1332
2060
|
if (cancelled) return;
|
|
1333
2061
|
const reviewWords = words.filter((w) => book[w.name]?.count).slice(0, cfg.chapterSize);
|
|
1334
2062
|
if (reviewWords.length === 0) {
|
|
1335
|
-
setErrorMsg(
|
|
2063
|
+
setErrorMsg(t.practice.errors.noMistakes);
|
|
1336
2064
|
setPhase("error");
|
|
1337
2065
|
return;
|
|
1338
2066
|
}
|
|
@@ -1342,7 +2070,7 @@ function PracticeScreen({ params }) {
|
|
|
1342
2070
|
}
|
|
1343
2071
|
const chapters = chunkChapters(words, cfg.chapterSize);
|
|
1344
2072
|
if (chapters.length === 0) {
|
|
1345
|
-
setErrorMsg(
|
|
2073
|
+
setErrorMsg(t.practice.errors.dictEmpty(dictId));
|
|
1346
2074
|
setPhase("error");
|
|
1347
2075
|
return;
|
|
1348
2076
|
}
|
|
@@ -1359,15 +2087,15 @@ function PracticeScreen({ params }) {
|
|
|
1359
2087
|
return () => {
|
|
1360
2088
|
cancelled = true;
|
|
1361
2089
|
};
|
|
1362
|
-
}, [dictId, chapterIndex, mode, cfg.chapterSize]);
|
|
2090
|
+
}, [dictId, chapterIndex, mode, cfg.chapterSize, t]);
|
|
1363
2091
|
if (phase === "loading") {
|
|
1364
|
-
return /* @__PURE__ */
|
|
2092
|
+
return /* @__PURE__ */ jsx10(Centered, { text: t.practice.loading, color: PALETTE.muted });
|
|
1365
2093
|
}
|
|
1366
2094
|
if (phase === "error") {
|
|
1367
|
-
return /* @__PURE__ */
|
|
2095
|
+
return /* @__PURE__ */ jsx10(ErrorView, { msg: errorMsg ?? t.practice.errors.unknown });
|
|
1368
2096
|
}
|
|
1369
2097
|
if (!loaded) return null;
|
|
1370
|
-
return /* @__PURE__ */
|
|
2098
|
+
return /* @__PURE__ */ jsx10(
|
|
1371
2099
|
PracticeRunner,
|
|
1372
2100
|
{
|
|
1373
2101
|
params,
|
|
@@ -1375,7 +2103,7 @@ function PracticeScreen({ params }) {
|
|
|
1375
2103
|
phase,
|
|
1376
2104
|
setPhase
|
|
1377
2105
|
},
|
|
1378
|
-
`${dictId}-${chapterIndex}-${mode}`
|
|
2106
|
+
`${dictId}-${chapterIndex}-${mode}-${params.stealth ? "s" : "n"}`
|
|
1379
2107
|
);
|
|
1380
2108
|
}
|
|
1381
2109
|
function PracticeRunner({
|
|
@@ -1385,19 +2113,22 @@ function PracticeRunner({
|
|
|
1385
2113
|
setPhase
|
|
1386
2114
|
}) {
|
|
1387
2115
|
const { dictId, chapterIndex, mode } = params;
|
|
2116
|
+
const stealth = params.stealth === true;
|
|
1388
2117
|
const { cfg } = useAppState();
|
|
1389
2118
|
const nav = useNav();
|
|
1390
2119
|
const { exit } = useApp3();
|
|
1391
2120
|
const goBack = () => nav.stack.length > 1 ? nav.back() : exit();
|
|
1392
2121
|
const persist = useSessionPersistence({ dictId, chapterIndex, mode });
|
|
2122
|
+
const dictName = useDictName(dictId);
|
|
1393
2123
|
const audio = useAudio({
|
|
1394
|
-
enabled: cfg.sounds.master,
|
|
2124
|
+
enabled: !stealth && cfg.sounds.master,
|
|
1395
2125
|
accent: cfg.accent,
|
|
1396
|
-
autoplayPronunciation: cfg.autoplayPronunciation
|
|
2126
|
+
autoplayPronunciation: !stealth && cfg.autoplayPronunciation
|
|
1397
2127
|
});
|
|
1398
2128
|
const finishedRef = useRef3(false);
|
|
1399
2129
|
const lastEffectRef = useRef3(null);
|
|
1400
2130
|
const lastIndexRef = useRef3(-1);
|
|
2131
|
+
const [infoVisible, setInfoVisible] = useState8(false);
|
|
1401
2132
|
const { session, lastEffect, tick } = useWordLoop({
|
|
1402
2133
|
playlist: loaded.playlist,
|
|
1403
2134
|
enabled: phase === "typing",
|
|
@@ -1410,12 +2141,13 @@ function PracticeRunner({
|
|
|
1410
2141
|
});
|
|
1411
2142
|
},
|
|
1412
2143
|
onEscape: () => setPhase(phase === "paused" ? "typing" : "paused"),
|
|
1413
|
-
onTab: () => {
|
|
2144
|
+
onTab: stealth ? void 0 : () => {
|
|
1414
2145
|
const cur = session.current ? loaded.playlist[session.current.wordIndex] : void 0;
|
|
1415
2146
|
if (cur) void audio.pronounce(cur.name);
|
|
1416
2147
|
}
|
|
1417
2148
|
});
|
|
1418
|
-
|
|
2149
|
+
useEffect6(() => {
|
|
2150
|
+
if (stealth) return;
|
|
1419
2151
|
if (lastEffect === null) return;
|
|
1420
2152
|
if (lastEffect === lastEffectRef.current) return;
|
|
1421
2153
|
lastEffectRef.current = lastEffect;
|
|
@@ -1425,8 +2157,9 @@ function PracticeRunner({
|
|
|
1425
2157
|
if (cfg.sounds.feedback) audio.correct();
|
|
1426
2158
|
if (cfg.sounds.keystroke) audio.keystroke();
|
|
1427
2159
|
}
|
|
1428
|
-
}, [lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
|
|
1429
|
-
|
|
2160
|
+
}, [stealth, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
|
|
2161
|
+
useEffect6(() => {
|
|
2162
|
+
if (stealth) return;
|
|
1430
2163
|
const idx = session.current?.wordIndex ?? -1;
|
|
1431
2164
|
if (idx === -1) return;
|
|
1432
2165
|
if (idx === lastIndexRef.current) return;
|
|
@@ -1435,63 +2168,148 @@ function PracticeRunner({
|
|
|
1435
2168
|
const next = loaded.playlist[idx + 1];
|
|
1436
2169
|
if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);
|
|
1437
2170
|
if (next) audio.prefetch(next.name);
|
|
1438
|
-
}, [session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
|
|
2171
|
+
}, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
|
|
1439
2172
|
void tick;
|
|
1440
2173
|
useInput3(
|
|
1441
|
-
(input) => {
|
|
1442
|
-
if (input === "
|
|
1443
|
-
|
|
2174
|
+
(input, key) => {
|
|
2175
|
+
if (key.ctrl && input === "i") {
|
|
2176
|
+
setInfoVisible((v) => !v);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
},
|
|
2180
|
+
{ isActive: stealth && phase === "typing" }
|
|
2181
|
+
);
|
|
2182
|
+
useInput3(
|
|
2183
|
+
(_input, key) => {
|
|
2184
|
+
if (key.return) {
|
|
2185
|
+
setPhase("typing");
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
if (key.escape) {
|
|
2189
|
+
goBack();
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
1444
2192
|
},
|
|
1445
2193
|
{ isActive: phase === "paused" }
|
|
1446
2194
|
);
|
|
1447
2195
|
useInput3(
|
|
1448
|
-
(input) => {
|
|
1449
|
-
if (
|
|
2196
|
+
(input, key) => {
|
|
2197
|
+
if (key.escape) {
|
|
1450
2198
|
goBack();
|
|
1451
2199
|
return;
|
|
1452
2200
|
}
|
|
1453
|
-
if (
|
|
2201
|
+
if (key.return) {
|
|
1454
2202
|
const nextIdx = chapterIndex + 1;
|
|
1455
2203
|
if (mode === "loop") {
|
|
1456
|
-
nav.replace({
|
|
2204
|
+
nav.replace({
|
|
2205
|
+
name: "practice",
|
|
2206
|
+
params: { dictId, chapterIndex, mode, stealth: params.stealth }
|
|
2207
|
+
});
|
|
1457
2208
|
} else if (mode === "review" || nextIdx >= loaded.totalChapters) {
|
|
1458
2209
|
goBack();
|
|
1459
2210
|
} else {
|
|
1460
|
-
nav.replace({
|
|
2211
|
+
nav.replace({
|
|
2212
|
+
name: "practice",
|
|
2213
|
+
params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth }
|
|
2214
|
+
});
|
|
1461
2215
|
}
|
|
1462
2216
|
return;
|
|
1463
2217
|
}
|
|
1464
2218
|
if (input === "m") {
|
|
1465
|
-
nav.replace({
|
|
2219
|
+
nav.replace({
|
|
2220
|
+
name: "practice",
|
|
2221
|
+
params: { dictId, chapterIndex: 0, mode: "review", stealth: params.stealth }
|
|
2222
|
+
});
|
|
1466
2223
|
return;
|
|
1467
2224
|
}
|
|
1468
2225
|
},
|
|
1469
2226
|
{ isActive: phase === "summary" }
|
|
1470
2227
|
);
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
2228
|
+
const completed = session.results.length;
|
|
2229
|
+
const errors = session.results.reduce((a, r) => a + r.errors, 0);
|
|
2230
|
+
const elapsedMs = Date.now() - session.startedAt;
|
|
2231
|
+
const minutes = elapsedMs / 6e4;
|
|
2232
|
+
const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
|
|
2233
|
+
const summary = phase === "summary" ? sessionSummary(session) : null;
|
|
2234
|
+
if (stealth) {
|
|
2235
|
+
if (phase === "paused") return /* @__PURE__ */ jsx10(StealthPaused, {});
|
|
2236
|
+
if (phase === "summary" && summary) {
|
|
2237
|
+
const sMinutes = summary.durationMs / 6e4;
|
|
2238
|
+
const sWpm = sMinutes > 0 ? Math.round(summary.wordCount / sMinutes * 10) / 10 : 0;
|
|
2239
|
+
const sErrWords = Object.keys(summary.perWordErrors).length;
|
|
2240
|
+
const sAcc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - sErrWords) / summary.wordCount);
|
|
2241
|
+
const sAccPct = Math.round(sAcc * 1e3) / 10;
|
|
2242
|
+
return /* @__PURE__ */ jsx10(
|
|
2243
|
+
StealthSummary,
|
|
2244
|
+
{
|
|
2245
|
+
wordCount: summary.wordCount,
|
|
2246
|
+
errors: summary.errors,
|
|
2247
|
+
durationMs: summary.durationMs,
|
|
2248
|
+
wpm: sWpm,
|
|
2249
|
+
accPct: sAccPct
|
|
2250
|
+
}
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
const currentWord2 = session.current ? loaded.playlist[session.current.wordIndex] : loaded.playlist[loaded.playlist.length - 1];
|
|
2254
|
+
const inputState2 = session.current?.input ?? { target: "", typed: "", errorsThisWord: 0 };
|
|
2255
|
+
const errWords = new Set(
|
|
2256
|
+
session.results.filter((r) => r.errors > 0).map((r) => r.word)
|
|
2257
|
+
).size;
|
|
2258
|
+
const accFrac = completed === 0 ? 1 : Math.max(0, (completed - errWords) / completed);
|
|
2259
|
+
const accPct = Math.round(accFrac * 1e3) / 10;
|
|
2260
|
+
const chapterLabel = mode === "review" ? "review" : `ch ${chapterIndex + 1}/${loaded.totalChapters}`;
|
|
2261
|
+
return /* @__PURE__ */ jsx10(
|
|
2262
|
+
StealthTyping,
|
|
2263
|
+
{
|
|
2264
|
+
target: currentWord2?.name ?? "",
|
|
2265
|
+
typed: inputState2.typed,
|
|
2266
|
+
hideTarget: mode === "dictation",
|
|
2267
|
+
phonetic: pickPhonetic(currentWord2, cfg.accent),
|
|
2268
|
+
translation: currentWord2?.trans ?? [],
|
|
2269
|
+
error: lastEffect === "wrong",
|
|
2270
|
+
info: {
|
|
2271
|
+
visible: infoVisible,
|
|
2272
|
+
dictName: truncateName(dictName, 24),
|
|
2273
|
+
chapterLabel,
|
|
2274
|
+
completed,
|
|
2275
|
+
total: loaded.playlist.length,
|
|
2276
|
+
wpm,
|
|
2277
|
+
accPct
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
2282
|
+
if (phase === "paused") {
|
|
2283
|
+
return /* @__PURE__ */ jsx10(
|
|
2284
|
+
PausedView,
|
|
2285
|
+
{
|
|
2286
|
+
dictName,
|
|
2287
|
+
chapterIndex,
|
|
2288
|
+
totalChapters: loaded.totalChapters,
|
|
2289
|
+
mode,
|
|
2290
|
+
completed,
|
|
2291
|
+
total: loaded.playlist.length
|
|
2292
|
+
}
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
if (phase === "summary" && summary) {
|
|
2296
|
+
return /* @__PURE__ */ jsx10(
|
|
1474
2297
|
SummaryView,
|
|
1475
2298
|
{
|
|
1476
|
-
|
|
2299
|
+
dictName,
|
|
1477
2300
|
chapterIndex,
|
|
1478
2301
|
totalChapters: loaded.totalChapters,
|
|
1479
2302
|
mode,
|
|
1480
|
-
summary
|
|
2303
|
+
summary
|
|
1481
2304
|
}
|
|
1482
2305
|
);
|
|
1483
2306
|
}
|
|
1484
2307
|
const currentWord = session.current ? loaded.playlist[session.current.wordIndex] : loaded.playlist[loaded.playlist.length - 1];
|
|
1485
2308
|
const inputState = session.current?.input ?? { target: "", typed: "", errorsThisWord: 0 };
|
|
1486
|
-
|
|
1487
|
-
const completed = session.results.length;
|
|
1488
|
-
const errors = session.results.reduce((a, r) => a + r.errors, 0);
|
|
1489
|
-
const minutes = elapsedMs / 6e4;
|
|
1490
|
-
const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
|
|
1491
|
-
return /* @__PURE__ */ jsx7(
|
|
2309
|
+
return /* @__PURE__ */ jsx10(
|
|
1492
2310
|
TypingLayout,
|
|
1493
2311
|
{
|
|
1494
|
-
|
|
2312
|
+
dictName,
|
|
1495
2313
|
chapterIndex,
|
|
1496
2314
|
totalChapters: loaded.totalChapters,
|
|
1497
2315
|
mode,
|
|
@@ -1515,19 +2333,20 @@ function pickPhonetic(word, accent) {
|
|
|
1515
2333
|
const p = accent === "us" ? word.usphone : word.ukphone;
|
|
1516
2334
|
return p ? `/${p}/` : null;
|
|
1517
2335
|
}
|
|
1518
|
-
function
|
|
2336
|
+
function fmtTime2(ms) {
|
|
1519
2337
|
const total = Math.floor(ms / 1e3);
|
|
1520
2338
|
const m = Math.floor(total / 60);
|
|
1521
2339
|
const s = total % 60;
|
|
1522
2340
|
return `${m}:${String(s).padStart(2, "0")}`;
|
|
1523
2341
|
}
|
|
1524
2342
|
function TypingLayout(props) {
|
|
2343
|
+
const t = useStrings();
|
|
1525
2344
|
const progressFrac = props.total === 0 ? 0 : props.completed / props.total;
|
|
1526
|
-
return /* @__PURE__ */
|
|
1527
|
-
/* @__PURE__ */
|
|
2345
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2346
|
+
/* @__PURE__ */ jsx10(
|
|
1528
2347
|
StatusBar,
|
|
1529
2348
|
{
|
|
1530
|
-
|
|
2349
|
+
dictName: props.dictName,
|
|
1531
2350
|
chapterIndex: props.chapterIndex,
|
|
1532
2351
|
totalChapters: props.totalChapters,
|
|
1533
2352
|
mode: props.mode,
|
|
@@ -1537,8 +2356,8 @@ function TypingLayout(props) {
|
|
|
1537
2356
|
elapsedMs: props.elapsedMs
|
|
1538
2357
|
}
|
|
1539
2358
|
),
|
|
1540
|
-
/* @__PURE__ */
|
|
1541
|
-
/* @__PURE__ */
|
|
2359
|
+
/* @__PURE__ */ jsxs4(Box5, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
|
|
2360
|
+
/* @__PURE__ */ jsx10(
|
|
1542
2361
|
BigWord,
|
|
1543
2362
|
{
|
|
1544
2363
|
target: props.target,
|
|
@@ -1547,34 +2366,41 @@ function TypingLayout(props) {
|
|
|
1547
2366
|
hideTarget: props.hideTarget
|
|
1548
2367
|
}
|
|
1549
2368
|
),
|
|
1550
|
-
props.phonetic && /* @__PURE__ */
|
|
1551
|
-
props.translation.length > 0 && /* @__PURE__ */
|
|
2369
|
+
props.phonetic && /* @__PURE__ */ jsx10(Box5, { marginTop: 3, children: /* @__PURE__ */ jsx10(Text4, { italic: true, color: PALETTE.muted, children: props.phonetic }) }),
|
|
2370
|
+
props.translation.length > 0 && /* @__PURE__ */ jsx10(Box5, { marginTop: 2, flexDirection: "column", alignItems: "center", children: props.translation.slice(0, 2).map((tr, i) => /* @__PURE__ */ jsx10(Text4, { color: PALETTE.primary, children: tr }, i)) })
|
|
1552
2371
|
] }),
|
|
1553
|
-
/* @__PURE__ */
|
|
1554
|
-
/* @__PURE__ */
|
|
1555
|
-
/* @__PURE__ */
|
|
2372
|
+
/* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
|
|
2373
|
+
/* @__PURE__ */ jsx10(ProgressBar, { frac: progressFrac }),
|
|
2374
|
+
/* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
|
|
1556
2375
|
props.completed,
|
|
1557
2376
|
"/",
|
|
1558
2377
|
props.total,
|
|
1559
2378
|
" \xB7 ",
|
|
1560
|
-
|
|
2379
|
+
fmtTime2(props.elapsedMs),
|
|
1561
2380
|
" \xB7 ",
|
|
1562
2381
|
props.wpm,
|
|
1563
|
-
"
|
|
2382
|
+
" ",
|
|
2383
|
+
t.practice.statCards.wpm,
|
|
2384
|
+
" \xB7 ",
|
|
1564
2385
|
props.errors,
|
|
1565
|
-
"
|
|
2386
|
+
" ",
|
|
2387
|
+
t.practice.statCards.errors
|
|
1566
2388
|
] }) }),
|
|
1567
|
-
/* @__PURE__ */
|
|
2389
|
+
/* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.footers.typing }) })
|
|
1568
2390
|
] })
|
|
1569
2391
|
] });
|
|
1570
2392
|
}
|
|
1571
2393
|
function StatusBar(props) {
|
|
1572
|
-
const
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
2394
|
+
const t = useStrings();
|
|
2395
|
+
const modeName = t.practice.modes[props.mode];
|
|
2396
|
+
const accentName = t.practice.accents[props.accent];
|
|
2397
|
+
const name = truncateName(props.dictName, 20);
|
|
2398
|
+
const left = props.mode === "review" ? `${name} \xB7 ${t.practice.reviewLabel} \xB7 ${accentName}` : `${name} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName} \xB7 ${accentName}`;
|
|
2399
|
+
const right = `${props.completed}/${props.total} \xB7 ${fmtTime2(props.elapsedMs)}`;
|
|
2400
|
+
return /* @__PURE__ */ jsxs4(Box5, { children: [
|
|
2401
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: left }),
|
|
2402
|
+
/* @__PURE__ */ jsx10(Box5, { flexGrow: 1 }),
|
|
2403
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: right })
|
|
1578
2404
|
] });
|
|
1579
2405
|
}
|
|
1580
2406
|
function ProgressBar({ frac }) {
|
|
@@ -1582,33 +2408,43 @@ function ProgressBar({ frac }) {
|
|
|
1582
2408
|
const width = Math.max(20, Math.min(72, cols - 16));
|
|
1583
2409
|
const filled = Math.round(width * Math.max(0, Math.min(1, frac)));
|
|
1584
2410
|
const empty = width - filled;
|
|
1585
|
-
return /* @__PURE__ */
|
|
1586
|
-
/* @__PURE__ */
|
|
1587
|
-
/* @__PURE__ */
|
|
2411
|
+
return /* @__PURE__ */ jsxs4(Box5, { justifyContent: "center", children: [
|
|
2412
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.accent, children: "\u2501".repeat(filled) }),
|
|
2413
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: "\u2500".repeat(empty) })
|
|
1588
2414
|
] });
|
|
1589
2415
|
}
|
|
1590
|
-
function PausedView() {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2416
|
+
function PausedView(props) {
|
|
2417
|
+
const t = useStrings();
|
|
2418
|
+
const frac = props.total === 0 ? 0 : props.completed / props.total;
|
|
2419
|
+
const subtitle = props.mode === "review" ? `${truncateName(props.dictName, 20)} \xB7 ${t.practice.reviewLabel}` : `${truncateName(props.dictName, 20)} \xB7 ${t.practice.pause.chapter(props.chapterIndex + 1, props.totalChapters)}`;
|
|
2420
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2421
|
+
/* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.warning, children: t.practice.pause.title }),
|
|
2422
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: subtitle }) }),
|
|
2423
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(ProgressBar, { frac }) }),
|
|
2424
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.pause.progress(props.completed, props.total) }) }),
|
|
2425
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.pause.hint }) })
|
|
1594
2426
|
] });
|
|
1595
2427
|
}
|
|
1596
2428
|
function ErrorView({ msg }) {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
/* @__PURE__ */
|
|
1600
|
-
/* @__PURE__ */
|
|
2429
|
+
const t = useStrings();
|
|
2430
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2431
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.error, children: msg }),
|
|
2432
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
|
|
2433
|
+
"Esc ",
|
|
2434
|
+
t.common.back
|
|
2435
|
+
] }) }),
|
|
2436
|
+
/* @__PURE__ */ jsx10(BackKey, {})
|
|
1601
2437
|
] });
|
|
1602
2438
|
}
|
|
1603
2439
|
function BackKey() {
|
|
1604
2440
|
const nav = useNav();
|
|
1605
|
-
useInput3((
|
|
1606
|
-
if (
|
|
2441
|
+
useInput3((_input, key) => {
|
|
2442
|
+
if (key.escape) nav.back();
|
|
1607
2443
|
});
|
|
1608
2444
|
return null;
|
|
1609
2445
|
}
|
|
1610
2446
|
function Centered({ text, color }) {
|
|
1611
|
-
return /* @__PURE__ */
|
|
2447
|
+
return /* @__PURE__ */ jsx10(Box5, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx10(Text4, { color, children: text }) });
|
|
1612
2448
|
}
|
|
1613
2449
|
function SummaryView(props) {
|
|
1614
2450
|
const { summary } = props;
|
|
@@ -1617,52 +2453,126 @@ function SummaryView(props) {
|
|
|
1617
2453
|
const errorWords = Object.keys(summary.perWordErrors).length;
|
|
1618
2454
|
const acc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - errorWords) / summary.wordCount);
|
|
1619
2455
|
const accPct = Math.round(acc * 1e3) / 10;
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
2456
|
+
const t = useStrings();
|
|
2457
|
+
const modeName = t.practice.modes[props.mode];
|
|
2458
|
+
const name = truncateName(props.dictName, 20);
|
|
2459
|
+
const subtitle = props.mode === "review" ? `${name} \xB7 ${t.practice.reviewLabel}` : `${name} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName}`;
|
|
2460
|
+
const nextLabel = props.mode === "loop" ? t.practice.summary.loopAgain : props.mode === "review" || props.chapterIndex + 1 >= props.totalChapters ? t.practice.summary.backMenu : t.practice.summary.nextChapter;
|
|
2461
|
+
const footer = `Enter ${nextLabel} \xB7 m ${t.practice.summary.reviewMistakes} \xB7 Esc ${t.practice.summary.backMenu}`;
|
|
2462
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 1, width: "100%", height: "100%", children: [
|
|
2463
|
+
/* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.success, children: t.practice.chapterComplete }),
|
|
2464
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: subtitle }) }),
|
|
2465
|
+
/* @__PURE__ */ jsxs4(Box5, { marginTop: 3, flexDirection: "row", justifyContent: "center", children: [
|
|
2466
|
+
/* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.words, value: String(summary.wordCount), color: PALETTE.text }),
|
|
2467
|
+
/* @__PURE__ */ jsx10(
|
|
1627
2468
|
StatCard,
|
|
1628
2469
|
{
|
|
1629
|
-
label:
|
|
2470
|
+
label: t.practice.statCards.errors,
|
|
1630
2471
|
value: String(summary.errors),
|
|
1631
2472
|
color: summary.errors > 0 ? PALETTE.error : PALETTE.muted
|
|
1632
2473
|
}
|
|
1633
2474
|
),
|
|
1634
|
-
/* @__PURE__ */
|
|
1635
|
-
/* @__PURE__ */
|
|
2475
|
+
/* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.wpm, value: String(wpm), color: PALETTE.accent }),
|
|
2476
|
+
/* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.accuracy, value: `${accPct}%`, color: PALETTE.accent })
|
|
1636
2477
|
] }),
|
|
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" }) })
|
|
2478
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.statCards.elapsed(fmtTime2(summary.durationMs)) }) }),
|
|
2479
|
+
/* @__PURE__ */ jsx10(Box5, { flexGrow: 1 }),
|
|
2480
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: footer }) })
|
|
1643
2481
|
] });
|
|
1644
2482
|
}
|
|
1645
2483
|
function StatCard({ label, value, color }) {
|
|
1646
|
-
return /* @__PURE__ */
|
|
1647
|
-
/* @__PURE__ */
|
|
1648
|
-
/* @__PURE__ */
|
|
2484
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", alignItems: "center", marginX: 3, children: [
|
|
2485
|
+
/* @__PURE__ */ jsx10(Text4, { bold: true, color, children: value }),
|
|
2486
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: label })
|
|
1649
2487
|
] });
|
|
1650
2488
|
}
|
|
1651
2489
|
|
|
1652
2490
|
// src/ui/screens/DictBrowser.tsx
|
|
1653
|
-
import { useEffect as
|
|
1654
|
-
import { Box as
|
|
1655
|
-
|
|
2491
|
+
import { useEffect as useEffect7, useMemo as useMemo2, useState as useState10 } from "react";
|
|
2492
|
+
import { Box as Box7, Text as Text6, useInput as useInput5, useStdout as useStdout3 } from "ink";
|
|
2493
|
+
|
|
2494
|
+
// src/ui/components/ActionPanel.tsx
|
|
2495
|
+
import { useState as useState9 } from "react";
|
|
2496
|
+
import { Box as Box6, Text as Text5, useInput as useInput4 } from "ink";
|
|
2497
|
+
import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2498
|
+
function ActionPanel({ title, items, onClose }) {
|
|
2499
|
+
const enabledIndices = items.map((it, i) => it.disabled ? -1 : i).filter((i) => i >= 0);
|
|
2500
|
+
const initial = enabledIndices[0] ?? 0;
|
|
2501
|
+
const [selected, setSelected] = useState9(initial);
|
|
2502
|
+
useInput4((input, key) => {
|
|
2503
|
+
if (key.escape) {
|
|
2504
|
+
onClose();
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
if (key.upArrow) {
|
|
2508
|
+
const cur = enabledIndices.indexOf(selected);
|
|
2509
|
+
const next = enabledIndices[(cur - 1 + enabledIndices.length) % enabledIndices.length];
|
|
2510
|
+
if (next !== void 0) setSelected(next);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
if (key.downArrow) {
|
|
2514
|
+
const cur = enabledIndices.indexOf(selected);
|
|
2515
|
+
const next = enabledIndices[(cur + 1) % enabledIndices.length];
|
|
2516
|
+
if (next !== void 0) setSelected(next);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
if (key.return) {
|
|
2520
|
+
const item = items[selected];
|
|
2521
|
+
if (item && !item.disabled) {
|
|
2522
|
+
void item.run();
|
|
2523
|
+
}
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
for (let i = 0; i < items.length; i++) {
|
|
2527
|
+
const it = items[i];
|
|
2528
|
+
if (it.disabled) continue;
|
|
2529
|
+
if (it.key && input === it.key) {
|
|
2530
|
+
void it.run();
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
const maxLabel = Math.max(...items.map((it) => it.label.length));
|
|
2536
|
+
const width = Math.max(maxLabel + 8, title.length + 4, 24);
|
|
2537
|
+
return /* @__PURE__ */ jsxs5(
|
|
2538
|
+
Box6,
|
|
2539
|
+
{
|
|
2540
|
+
flexDirection: "column",
|
|
2541
|
+
borderStyle: "round",
|
|
2542
|
+
borderColor: PALETTE.accent,
|
|
2543
|
+
paddingX: 2,
|
|
2544
|
+
paddingY: 1,
|
|
2545
|
+
width,
|
|
2546
|
+
children: [
|
|
2547
|
+
/* @__PURE__ */ jsx11(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx11(Text5, { bold: true, color: PALETTE.accent, children: title }) }),
|
|
2548
|
+
items.map((it, i) => {
|
|
2549
|
+
const active = i === selected;
|
|
2550
|
+
const color = it.disabled ? PALETTE.muted : active ? PALETTE.text : PALETTE.muted;
|
|
2551
|
+
return /* @__PURE__ */ jsxs5(Box6, { children: [
|
|
2552
|
+
/* @__PURE__ */ jsx11(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2553
|
+
/* @__PURE__ */ jsx11(Text5, { bold: active, color, children: it.label })
|
|
2554
|
+
] }, i);
|
|
2555
|
+
}),
|
|
2556
|
+
/* @__PURE__ */ jsx11(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text5, { color: PALETTE.muted, children: "\u2191/\u2193 \xB7 Enter \xB7 Esc" }) })
|
|
2557
|
+
]
|
|
2558
|
+
}
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// src/ui/screens/DictBrowser.tsx
|
|
2563
|
+
import { Fragment, jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1656
2564
|
function DictBrowser({ params }) {
|
|
1657
2565
|
const nav = useNav();
|
|
1658
2566
|
const { cfg, setCfg } = useAppState();
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
1661
|
-
const [
|
|
1662
|
-
const [
|
|
1663
|
-
const [
|
|
1664
|
-
const [
|
|
1665
|
-
const [
|
|
2567
|
+
const t = useStrings();
|
|
2568
|
+
const { stdout } = useStdout3();
|
|
2569
|
+
const [rows, setRows] = useState10([]);
|
|
2570
|
+
const [loading, setLoading] = useState10(true);
|
|
2571
|
+
const [selected, setSelected] = useState10(0);
|
|
2572
|
+
const [filter, setFilter] = useState10("");
|
|
2573
|
+
const [pending, setPending] = useState10(null);
|
|
2574
|
+
const [tick, setTick] = useState10(0);
|
|
2575
|
+
const [panel, setPanel] = useState10(null);
|
|
1666
2576
|
const refresh = async () => {
|
|
1667
2577
|
const reg = await loadRegistry();
|
|
1668
2578
|
const flagged = await Promise.all(
|
|
@@ -1671,168 +2581,227 @@ function DictBrowser({ params }) {
|
|
|
1671
2581
|
setRows(flagged);
|
|
1672
2582
|
setLoading(false);
|
|
1673
2583
|
};
|
|
1674
|
-
|
|
2584
|
+
useEffect7(() => {
|
|
1675
2585
|
void refresh();
|
|
1676
2586
|
}, [tick]);
|
|
1677
|
-
const filtered =
|
|
2587
|
+
const filtered = useMemo2(
|
|
2588
|
+
() => filter ? rows.filter((r) => filterRegistry([r.entry], filter).length > 0) : rows,
|
|
2589
|
+
[filter, rows]
|
|
2590
|
+
);
|
|
1678
2591
|
const safeSelected = Math.max(0, Math.min(filtered.length - 1, selected));
|
|
1679
2592
|
const current = filtered[safeSelected];
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
2593
|
+
const rowsTotal = stdout?.rows ?? 24;
|
|
2594
|
+
const visibleH = Math.max(6, rowsTotal - 8);
|
|
2595
|
+
const half = Math.floor(visibleH / 2);
|
|
2596
|
+
const start2 = Math.max(0, Math.min(filtered.length - visibleH, safeSelected - half));
|
|
2597
|
+
const end = Math.min(filtered.length, start2 + visibleH);
|
|
2598
|
+
const goPractice = (id) => {
|
|
2599
|
+
nav.replace({
|
|
2600
|
+
name: "practice",
|
|
2601
|
+
params: {
|
|
2602
|
+
dictId: id,
|
|
2603
|
+
chapterIndex: 0,
|
|
2604
|
+
mode: cfg.defaultMode,
|
|
2605
|
+
stealth: cfg.stealth === "default"
|
|
1685
2606
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2607
|
+
});
|
|
2608
|
+
};
|
|
2609
|
+
const doSetDefault = async (id, navigate = true) => {
|
|
2610
|
+
await setCfg({ ...cfg, defaultDict: id });
|
|
2611
|
+
setPanel(null);
|
|
2612
|
+
if (navigate) {
|
|
2613
|
+
if (params?.pickerMode === "choose-then-practice") {
|
|
2614
|
+
goPractice(id);
|
|
2615
|
+
} else {
|
|
2616
|
+
nav.back();
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
const doDelete = (id) => {
|
|
2621
|
+
setPanel(null);
|
|
2622
|
+
setPending({ kind: "removing", id });
|
|
2623
|
+
void (async () => {
|
|
2624
|
+
try {
|
|
2625
|
+
await removeDictionary(id);
|
|
2626
|
+
setPending(null);
|
|
2627
|
+
setTick((n) => n + 1);
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
setPending({ kind: "error", id, msg: err.message });
|
|
1689
2630
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
2631
|
+
})();
|
|
2632
|
+
};
|
|
2633
|
+
const doPull = (id) => {
|
|
2634
|
+
setPanel(null);
|
|
2635
|
+
setPending({ kind: "pulling", id });
|
|
2636
|
+
void (async () => {
|
|
2637
|
+
try {
|
|
2638
|
+
await pullDictionary(id);
|
|
2639
|
+
setPending(null);
|
|
2640
|
+
setTick((n) => n + 1);
|
|
2641
|
+
} catch (err) {
|
|
2642
|
+
setPending({ kind: "error", id, msg: err.message });
|
|
1692
2643
|
}
|
|
2644
|
+
})();
|
|
2645
|
+
};
|
|
2646
|
+
const doRefreshList = () => {
|
|
2647
|
+
setPanel(null);
|
|
2648
|
+
setPending({ kind: "refreshing" });
|
|
2649
|
+
setTick((n) => n + 1);
|
|
2650
|
+
setPending(null);
|
|
2651
|
+
};
|
|
2652
|
+
useInput5((input, key) => {
|
|
2653
|
+
if (panel !== null) return;
|
|
2654
|
+
if (key.escape) {
|
|
2655
|
+
nav.back();
|
|
1693
2656
|
return;
|
|
1694
2657
|
}
|
|
1695
|
-
if (key.upArrow)
|
|
1696
|
-
|
|
1697
|
-
if (input === "/") {
|
|
1698
|
-
setFilterFocus(true);
|
|
2658
|
+
if (key.upArrow) {
|
|
2659
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
1699
2660
|
return;
|
|
1700
2661
|
}
|
|
1701
|
-
if (key.
|
|
1702
|
-
|
|
2662
|
+
if (key.downArrow) {
|
|
2663
|
+
setSelected((i) => Math.min(filtered.length - 1, i + 1));
|
|
1703
2664
|
return;
|
|
1704
2665
|
}
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
void (async () => {
|
|
1708
|
-
await setCfg({ ...cfg, defaultDict: current.entry.id });
|
|
1709
|
-
if (params?.pickerMode === "choose-then-practice") {
|
|
1710
|
-
nav.replace({
|
|
1711
|
-
name: "practice",
|
|
1712
|
-
params: { dictId: current.entry.id, chapterIndex: 0, mode: cfg.defaultMode }
|
|
1713
|
-
});
|
|
1714
|
-
} else {
|
|
1715
|
-
nav.back();
|
|
1716
|
-
}
|
|
1717
|
-
})();
|
|
2666
|
+
if (key.ctrl && input === "k") {
|
|
2667
|
+
setPanel("more");
|
|
1718
2668
|
return;
|
|
1719
2669
|
}
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
1722
|
-
name: "practice",
|
|
1723
|
-
params: { dictId: current.entry.id, chapterIndex: 0, mode: cfg.defaultMode }
|
|
1724
|
-
});
|
|
2670
|
+
if (key.return) {
|
|
2671
|
+
if (current) setPanel("item");
|
|
1725
2672
|
return;
|
|
1726
2673
|
}
|
|
1727
|
-
if (
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
try {
|
|
1731
|
-
await removeDictionary(current.entry.id);
|
|
1732
|
-
setPending(null);
|
|
1733
|
-
setTick((t) => t + 1);
|
|
1734
|
-
} catch (err) {
|
|
1735
|
-
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
1736
|
-
}
|
|
1737
|
-
})();
|
|
2674
|
+
if (key.backspace || key.delete) {
|
|
2675
|
+
setFilter((f) => f.slice(0, -1));
|
|
2676
|
+
setSelected(0);
|
|
1738
2677
|
return;
|
|
1739
2678
|
}
|
|
1740
|
-
if (input ===
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
try {
|
|
1744
|
-
await pullDictionary(current.entry.id);
|
|
1745
|
-
setPending(null);
|
|
1746
|
-
setTick((t) => t + 1);
|
|
1747
|
-
} catch (err) {
|
|
1748
|
-
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
1749
|
-
}
|
|
1750
|
-
})();
|
|
2679
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
2680
|
+
setFilter((f) => f + input);
|
|
2681
|
+
setSelected(0);
|
|
1751
2682
|
}
|
|
1752
2683
|
});
|
|
1753
2684
|
if (loading) {
|
|
1754
|
-
return /* @__PURE__ */
|
|
2685
|
+
return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.loading }) });
|
|
1755
2686
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2687
|
+
const itemPanelItems = current ? [
|
|
2688
|
+
{
|
|
2689
|
+
label: t.dict.action.setDefault,
|
|
2690
|
+
run: () => void doSetDefault(current.entry.id, params?.pickerMode !== void 0)
|
|
2691
|
+
},
|
|
2692
|
+
{
|
|
2693
|
+
label: t.dict.action.practice,
|
|
2694
|
+
run: () => goPractice(current.entry.id)
|
|
2695
|
+
},
|
|
2696
|
+
{
|
|
2697
|
+
label: t.dict.action.delete,
|
|
2698
|
+
disabled: !current.local,
|
|
2699
|
+
run: () => doDelete(current.entry.id)
|
|
2700
|
+
},
|
|
2701
|
+
{ label: t.common.cancel, run: () => setPanel(null) }
|
|
2702
|
+
] : [];
|
|
2703
|
+
const morePanelItems = [
|
|
2704
|
+
{
|
|
2705
|
+
label: t.dict.command.pull,
|
|
2706
|
+
disabled: !current,
|
|
2707
|
+
run: () => current && doPull(current.entry.id)
|
|
2708
|
+
},
|
|
2709
|
+
{
|
|
2710
|
+
label: t.dict.command.import,
|
|
2711
|
+
disabled: true,
|
|
2712
|
+
run: () => void 0
|
|
2713
|
+
},
|
|
2714
|
+
{ label: t.dict.command.refreshList, run: () => doRefreshList() },
|
|
2715
|
+
{ label: t.common.cancel, run: () => setPanel(null) }
|
|
2716
|
+
];
|
|
2717
|
+
if (panel === "item" && current) {
|
|
2718
|
+
return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(
|
|
2719
|
+
ActionPanel,
|
|
2720
|
+
{
|
|
2721
|
+
title: `${t.dict.action.title} \xB7 ${current.entry.name}`,
|
|
2722
|
+
items: itemPanelItems,
|
|
2723
|
+
onClose: () => setPanel(null)
|
|
2724
|
+
}
|
|
2725
|
+
) });
|
|
2726
|
+
}
|
|
2727
|
+
if (panel === "more") {
|
|
2728
|
+
return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(
|
|
2729
|
+
ActionPanel,
|
|
2730
|
+
{
|
|
2731
|
+
title: t.dict.command.title,
|
|
2732
|
+
items: morePanelItems,
|
|
2733
|
+
onClose: () => setPanel(null)
|
|
2734
|
+
}
|
|
2735
|
+
) });
|
|
2736
|
+
}
|
|
2737
|
+
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2738
|
+
/* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2739
|
+
/* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.accent, children: t.dict.title }),
|
|
2740
|
+
/* @__PURE__ */ jsx12(Box7, { flexGrow: 1 }),
|
|
2741
|
+
/* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: filter ? `${t.dict.filterPlaceholder}: ${filter}_` : `${t.dict.filterPlaceholder}_` }),
|
|
2742
|
+
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2743
|
+
" ",
|
|
2744
|
+
t.dict.entries(filtered.length)
|
|
2745
|
+
] })
|
|
1761
2746
|
] }),
|
|
1762
|
-
/* @__PURE__ */
|
|
1763
|
-
/* @__PURE__ */
|
|
1764
|
-
const i =
|
|
2747
|
+
/* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexGrow: 1, children: [
|
|
2748
|
+
/* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "75%", paddingRight: 1, children: filtered.slice(start2, end).map((row, vi) => {
|
|
2749
|
+
const i = start2 + vi;
|
|
1765
2750
|
const active = i === safeSelected;
|
|
1766
2751
|
const isDefault = cfg.defaultDict === row.entry.id;
|
|
1767
|
-
return /* @__PURE__ */
|
|
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) })
|
|
2752
|
+
return /* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2753
|
+
/* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }) }),
|
|
2754
|
+
/* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: row.local ? PALETTE.accent : PALETTE.muted, children: row.local ? "\u25CF" : "\u25CB" }) }),
|
|
2755
|
+
/* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: isDefault ? PALETTE.success : PALETTE.muted, children: isDefault ? "\u2605" : " " }) }),
|
|
2756
|
+
/* @__PURE__ */ jsx12(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx12(Text6, { bold: active, color: active ? PALETTE.text : PALETTE.muted, wrap: "truncate", children: row.entry.name }) }),
|
|
2757
|
+
/* @__PURE__ */ jsx12(Box7, { width: 6, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: String(row.entry.length).padStart(5) }) })
|
|
1775
2758
|
] }, row.entry.id);
|
|
1776
2759
|
}) }),
|
|
1777
|
-
/* @__PURE__ */
|
|
1778
|
-
/* @__PURE__ */
|
|
1779
|
-
/* @__PURE__ */
|
|
1780
|
-
/* @__PURE__ */
|
|
2760
|
+
/* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "25%", paddingLeft: 1, children: current && /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
2761
|
+
/* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.text, wrap: "wrap", children: current.entry.name }),
|
|
2762
|
+
/* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: current.entry.id }),
|
|
2763
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, wrap: "wrap", children: [
|
|
1781
2764
|
current.entry.language,
|
|
1782
2765
|
" \xB7 ",
|
|
1783
|
-
current.entry.category
|
|
1784
|
-
" \xB7 ",
|
|
1785
|
-
current.entry.length,
|
|
1786
|
-
" words"
|
|
1787
|
-
] }) }),
|
|
1788
|
-
/* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text4, { color: PALETTE.primary, children: current.entry.description || "(no description)" }) }),
|
|
1789
|
-
current.entry.tags.length > 0 && /* @__PURE__ */ jsx8(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.muted, children: [
|
|
1790
|
-
"tags: ",
|
|
1791
|
-
current.entry.tags.join(", ")
|
|
2766
|
+
current.entry.category
|
|
1792
2767
|
] }) }),
|
|
1793
|
-
/* @__PURE__ */
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2768
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.wordsLabel(current.entry.length) }) }),
|
|
2769
|
+
current.entry.description && /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.primary, wrap: "wrap", children: current.entry.description }) }),
|
|
2770
|
+
current.entry.tags.length > 0 && /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, wrap: "wrap", children: t.dict.tagsLabel(current.entry.tags.join(", ")) }) }),
|
|
2771
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: current.local ? PALETTE.accent : PALETTE.muted, children: current.local ? t.dict.local : t.dict.notLocal }) }),
|
|
2772
|
+
cfg.defaultDict === current.entry.id && /* @__PURE__ */ jsx12(Box7, { children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.success, children: t.dict.defaultMark }) })
|
|
1797
2773
|
] }) })
|
|
1798
2774
|
] }),
|
|
1799
|
-
pending && /* @__PURE__ */
|
|
1800
|
-
pending.kind === "pulling" && /* @__PURE__ */
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
] }),
|
|
1805
|
-
pending.kind === "removing" && /* @__PURE__ */ jsxs4(Text4, { color: PALETTE.warning, children: [
|
|
1806
|
-
"removing ",
|
|
1807
|
-
pending.id,
|
|
2775
|
+
pending && /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, children: [
|
|
2776
|
+
pending.kind === "pulling" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.warning, children: t.dict.pulling(pending.id) }),
|
|
2777
|
+
pending.kind === "removing" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.warning, children: t.dict.removing(pending.id) }),
|
|
2778
|
+
pending.kind === "refreshing" && /* @__PURE__ */ jsxs6(Text6, { color: PALETTE.warning, children: [
|
|
2779
|
+
t.dict.command.refreshList,
|
|
1808
2780
|
"\u2026"
|
|
1809
2781
|
] }),
|
|
1810
|
-
pending.kind === "error" && /* @__PURE__ */
|
|
1811
|
-
"error on ",
|
|
1812
|
-
pending.id,
|
|
1813
|
-
": ",
|
|
1814
|
-
pending.msg
|
|
1815
|
-
] })
|
|
2782
|
+
pending.kind === "error" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.error, children: t.dict.errorOn(pending.id, pending.msg) })
|
|
1816
2783
|
] }),
|
|
1817
|
-
/* @__PURE__ */
|
|
2784
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.footer }) })
|
|
1818
2785
|
] });
|
|
1819
2786
|
}
|
|
1820
2787
|
|
|
1821
2788
|
// src/ui/screens/ConfigEditor.tsx
|
|
1822
|
-
import { useState as
|
|
1823
|
-
import { Box as
|
|
1824
|
-
import { jsx as
|
|
2789
|
+
import { useState as useState11 } from "react";
|
|
2790
|
+
import { Box as Box8, Text as Text7, useInput as useInput6 } from "ink";
|
|
2791
|
+
import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1825
2792
|
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: "
|
|
1833
|
-
{ kind: "bool", path: "
|
|
1834
|
-
{ kind: "bool", path: "sounds.
|
|
1835
|
-
{ kind: "
|
|
2793
|
+
{ kind: "dictRef", path: "defaultDict", labelKey: "defaultDict" },
|
|
2794
|
+
{ kind: "enum", path: "defaultMode", labelKey: "defaultMode", options: ["order", "dictation", "review", "random", "loop"] },
|
|
2795
|
+
{ kind: "enum", path: "accent", labelKey: "accent", options: ["us", "uk"] },
|
|
2796
|
+
{ kind: "enum", path: "language", labelKey: "language", options: ["auto", "zh", "en"] },
|
|
2797
|
+
{ kind: "enum", path: "mirror", labelKey: "mirror", options: ["jsdelivr", "github"] },
|
|
2798
|
+
{ kind: "enum", path: "stealth", labelKey: "stealth", options: ["off", "menu", "default"] },
|
|
2799
|
+
{ kind: "int", path: "chapterSize", labelKey: "chapterSize", min: 1, max: 200 },
|
|
2800
|
+
{ kind: "bool", path: "autoplayPronunciation", labelKey: "autoplayPronunciation" },
|
|
2801
|
+
{ kind: "bool", path: "sounds.master", labelKey: "soundsMaster" },
|
|
2802
|
+
{ kind: "bool", path: "sounds.keystroke", labelKey: "soundsKeystroke" },
|
|
2803
|
+
{ kind: "bool", path: "sounds.feedback", labelKey: "soundsFeedback" },
|
|
2804
|
+
{ kind: "string", path: "sounds.keySoundName", labelKey: "soundsKeySound" }
|
|
1836
2805
|
];
|
|
1837
2806
|
function getByPath2(cfg, path) {
|
|
1838
2807
|
return path.split(".").reduce((acc, k) => {
|
|
@@ -1843,10 +2812,12 @@ function getByPath2(cfg, path) {
|
|
|
1843
2812
|
function ConfigEditor() {
|
|
1844
2813
|
const nav = useNav();
|
|
1845
2814
|
const { cfg, setCfg } = useAppState();
|
|
1846
|
-
const
|
|
1847
|
-
const
|
|
1848
|
-
const [
|
|
1849
|
-
const [
|
|
2815
|
+
const t = useStrings();
|
|
2816
|
+
const defaultDictName = useDictName(cfg.defaultDict);
|
|
2817
|
+
const [selected, setSelected] = useState11(0);
|
|
2818
|
+
const [editing, setEditing] = useState11(false);
|
|
2819
|
+
const [draft, setDraft] = useState11("");
|
|
2820
|
+
const [error, setError] = useState11(null);
|
|
1850
2821
|
const field = FIELDS[selected];
|
|
1851
2822
|
const currentValue = getByPath2(cfg, field.path);
|
|
1852
2823
|
const commit = async (raw) => {
|
|
@@ -1859,7 +2830,7 @@ function ConfigEditor() {
|
|
|
1859
2830
|
setError(err.message);
|
|
1860
2831
|
}
|
|
1861
2832
|
};
|
|
1862
|
-
|
|
2833
|
+
useInput6((input, key) => {
|
|
1863
2834
|
if (editing && field.kind === "string") {
|
|
1864
2835
|
if (key.escape) {
|
|
1865
2836
|
setEditing(false);
|
|
@@ -1894,7 +2865,7 @@ function ConfigEditor() {
|
|
|
1894
2865
|
if (/^[0-9]$/.test(input)) setDraft((d) => d + input);
|
|
1895
2866
|
return;
|
|
1896
2867
|
}
|
|
1897
|
-
if (key.escape
|
|
2868
|
+
if (key.escape) {
|
|
1898
2869
|
nav.back();
|
|
1899
2870
|
return;
|
|
1900
2871
|
}
|
|
@@ -1929,73 +2900,99 @@ function ConfigEditor() {
|
|
|
1929
2900
|
setError(null);
|
|
1930
2901
|
}
|
|
1931
2902
|
});
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
/* @__PURE__ */
|
|
2903
|
+
const labelW = Math.max(...FIELDS.map((f) => visibleWidth2(t.config.fields[f.labelKey]))) + 4;
|
|
2904
|
+
return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2905
|
+
/* @__PURE__ */ jsx13(Text7, { bold: true, color: PALETTE.accent, children: t.config.title }),
|
|
2906
|
+
/* @__PURE__ */ jsx13(Box8, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: FIELDS.map((f, i) => {
|
|
1935
2907
|
const active = i === selected;
|
|
1936
2908
|
const value = getByPath2(cfg, f.path);
|
|
1937
|
-
const display = renderValue(
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2909
|
+
const display = renderValue(
|
|
2910
|
+
f,
|
|
2911
|
+
value,
|
|
2912
|
+
active && editing ? draft : null,
|
|
2913
|
+
t,
|
|
2914
|
+
f.path === "defaultDict" ? defaultDictName : ""
|
|
2915
|
+
);
|
|
2916
|
+
const label = t.config.fields[f.labelKey];
|
|
2917
|
+
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
|
|
2918
|
+
return /* @__PURE__ */ jsxs7(Box8, { children: [
|
|
2919
|
+
/* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2920
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
|
|
2921
|
+
label,
|
|
2922
|
+
pad
|
|
2923
|
+
] }),
|
|
2924
|
+
/* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: display })
|
|
1942
2925
|
] }, f.path);
|
|
1943
2926
|
}) }),
|
|
1944
|
-
error && /* @__PURE__ */
|
|
2927
|
+
error && /* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.error, children: [
|
|
1945
2928
|
"! ",
|
|
1946
2929
|
error
|
|
1947
2930
|
] }) }),
|
|
1948
|
-
/* @__PURE__ */
|
|
2931
|
+
/* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text7, { color: PALETTE.muted, children: hintFor(field, editing, t) }) })
|
|
1949
2932
|
] });
|
|
1950
2933
|
}
|
|
1951
|
-
function renderValue(field, value, draft) {
|
|
2934
|
+
function renderValue(field, value, draft, t, dictDisplayName) {
|
|
1952
2935
|
if (draft !== null) return `${draft}_`;
|
|
1953
|
-
if (field.kind === "bool") return value ?
|
|
1954
|
-
if (field.kind === "dictRef")
|
|
1955
|
-
|
|
2936
|
+
if (field.kind === "bool") return value ? `\u2713 ${t.common.on}` : `\u2717 ${t.common.off}`;
|
|
2937
|
+
if (field.kind === "dictRef") {
|
|
2938
|
+
if (!value) return "\u2014";
|
|
2939
|
+
return truncateName(dictDisplayName || String(value), 24);
|
|
2940
|
+
}
|
|
2941
|
+
if (field.kind === "enum") {
|
|
2942
|
+
if (field.path === "stealth") {
|
|
2943
|
+
const v = String(value);
|
|
2944
|
+
const label = t.config.enumValues.stealth[v] ?? String(value);
|
|
2945
|
+
return `< ${label} >`;
|
|
2946
|
+
}
|
|
2947
|
+
return `< ${value} >`;
|
|
2948
|
+
}
|
|
1956
2949
|
return String(value ?? "");
|
|
1957
2950
|
}
|
|
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
|
|
2951
|
+
function hintFor(field, editing, t) {
|
|
2952
|
+
if (editing) return t.config.hints.editing;
|
|
2953
|
+
if (field.kind === "bool") return t.config.hints.bool;
|
|
2954
|
+
if (field.kind === "enum") return t.config.hints.enum;
|
|
2955
|
+
if (field.kind === "dictRef") return t.config.hints.dictRef;
|
|
2956
|
+
return t.config.hints.stringOrInt;
|
|
1964
2957
|
}
|
|
1965
2958
|
|
|
1966
2959
|
// src/ui/screens/StatsViewer.tsx
|
|
1967
|
-
import { useEffect as
|
|
1968
|
-
import { Box as
|
|
1969
|
-
import { jsx as
|
|
2960
|
+
import { useEffect as useEffect8, useState as useState12 } from "react";
|
|
2961
|
+
import { Box as Box9, Text as Text8, useInput as useInput7 } from "ink";
|
|
2962
|
+
import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1970
2963
|
var DAY_WINDOWS = [7, 14, 30, 90];
|
|
1971
2964
|
function StatsViewer() {
|
|
1972
2965
|
const nav = useNav();
|
|
1973
|
-
const
|
|
1974
|
-
const [
|
|
1975
|
-
const [
|
|
1976
|
-
|
|
2966
|
+
const t = useStrings();
|
|
2967
|
+
const [sessions, setSessions] = useState12(null);
|
|
2968
|
+
const [book, setBook] = useState12(null);
|
|
2969
|
+
const [windowIdx, setWindowIdx] = useState12(1);
|
|
2970
|
+
useEffect8(() => {
|
|
1977
2971
|
void (async () => {
|
|
1978
2972
|
const [s, b] = await Promise.all([loadSessions(), loadMistakes()]);
|
|
1979
2973
|
setSessions(s);
|
|
1980
2974
|
setBook(b);
|
|
1981
2975
|
})();
|
|
1982
2976
|
}, []);
|
|
1983
|
-
|
|
1984
|
-
if (key.escape
|
|
2977
|
+
useInput7((_input, key) => {
|
|
2978
|
+
if (key.escape) {
|
|
1985
2979
|
nav.back();
|
|
1986
2980
|
return;
|
|
1987
2981
|
}
|
|
1988
|
-
if (
|
|
1989
|
-
if (
|
|
2982
|
+
if (key.rightArrow) setWindowIdx((i) => (i + 1) % DAY_WINDOWS.length);
|
|
2983
|
+
if (key.leftArrow) setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);
|
|
1990
2984
|
});
|
|
1991
2985
|
if (!sessions || !book) {
|
|
1992
|
-
return /* @__PURE__ */
|
|
2986
|
+
return /* @__PURE__ */ jsx14(Box9, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.loading }) });
|
|
1993
2987
|
}
|
|
1994
2988
|
if (sessions.length === 0) {
|
|
1995
|
-
return /* @__PURE__ */
|
|
1996
|
-
/* @__PURE__ */
|
|
1997
|
-
/* @__PURE__ */
|
|
1998
|
-
/* @__PURE__ */
|
|
2989
|
+
return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2990
|
+
/* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.none }),
|
|
2991
|
+
/* @__PURE__ */ jsx14(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.nonePractice }) }),
|
|
2992
|
+
/* @__PURE__ */ jsx14(Box9, { marginTop: 2, children: /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
|
|
2993
|
+
"Esc ",
|
|
2994
|
+
t.common.back
|
|
2995
|
+
] }) })
|
|
1999
2996
|
] });
|
|
2000
2997
|
}
|
|
2001
2998
|
const days = DAY_WINDOWS[windowIdx];
|
|
@@ -2012,102 +3009,162 @@ function StatsViewer() {
|
|
|
2012
3009
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
2013
3010
|
const recent = sessions.slice(-5).reverse();
|
|
2014
3011
|
const top = topN(book, 8);
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
/* @__PURE__ */
|
|
2024
|
-
/* @__PURE__ */
|
|
2025
|
-
/* @__PURE__ */
|
|
3012
|
+
const wpms = buckets.map((b) => b.wpm);
|
|
3013
|
+
const accs = buckets.map((b) => b.accuracy * 100);
|
|
3014
|
+
const ses = buckets.map((b) => b.sessions);
|
|
3015
|
+
return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
3016
|
+
/* @__PURE__ */ jsx14(Text8, { bold: true, color: PALETTE.accent, children: t.stats.title }),
|
|
3017
|
+
/* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
3018
|
+
/* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.lifetime }),
|
|
3019
|
+
/* @__PURE__ */ jsxs8(Box9, { marginTop: 1, children: [
|
|
3020
|
+
/* @__PURE__ */ jsx14(Stat, { label: t.stats.sessions, value: String(sessions.length) }),
|
|
3021
|
+
/* @__PURE__ */ jsx14(Stat, { label: t.stats.words, value: String(totalWords) }),
|
|
3022
|
+
/* @__PURE__ */ jsx14(Stat, { label: t.stats.errors, value: String(totalErrors) }),
|
|
3023
|
+
/* @__PURE__ */ jsx14(Stat, { label: t.stats.wpm, value: String(overallWpm), accent: true }),
|
|
3024
|
+
/* @__PURE__ */ jsx14(Stat, { label: t.stats.accuracy, value: `${Math.round(overallAcc * 1e3) / 10}%`, accent: true }),
|
|
3025
|
+
/* @__PURE__ */ jsx14(Stat, { label: t.stats.streak, value: `${streak}d`, accent: true })
|
|
2026
3026
|
] })
|
|
2027
3027
|
] }),
|
|
2028
|
-
/* @__PURE__ */
|
|
2029
|
-
/* @__PURE__ */
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
3028
|
+
/* @__PURE__ */ jsxs8(Box9, { marginTop: 2, flexDirection: "column", children: [
|
|
3029
|
+
/* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.last(days) }),
|
|
3030
|
+
/* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: PALETTE.muted, paddingX: 1, children: [
|
|
3031
|
+
/* @__PURE__ */ jsx14(
|
|
3032
|
+
SparkRow,
|
|
3033
|
+
{
|
|
3034
|
+
label: t.stats.wpm,
|
|
3035
|
+
values: wpms,
|
|
3036
|
+
maxLabel: t.stats.maxLabel
|
|
3037
|
+
}
|
|
3038
|
+
),
|
|
3039
|
+
/* @__PURE__ */ jsx14(
|
|
3040
|
+
SparkRow,
|
|
3041
|
+
{
|
|
3042
|
+
label: t.stats.accuracy,
|
|
3043
|
+
values: accs,
|
|
3044
|
+
maxLabel: t.stats.maxLabel,
|
|
3045
|
+
suffix: "%"
|
|
3046
|
+
}
|
|
3047
|
+
),
|
|
3048
|
+
/* @__PURE__ */ jsx14(
|
|
3049
|
+
SparkRow,
|
|
3050
|
+
{
|
|
3051
|
+
label: t.stats.sessions,
|
|
3052
|
+
values: ses,
|
|
3053
|
+
maxLabel: t.stats.maxLabel
|
|
3054
|
+
}
|
|
3055
|
+
)
|
|
2051
3056
|
] })
|
|
2052
3057
|
] }),
|
|
2053
|
-
/* @__PURE__ */
|
|
2054
|
-
/* @__PURE__ */
|
|
2055
|
-
recent.map((s, i) => /* @__PURE__ */
|
|
2056
|
-
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2057
|
-
" ",
|
|
2058
|
-
s.ts.replace("T", " ").slice(0, 16),
|
|
2059
|
-
" "
|
|
2060
|
-
] }),
|
|
2061
|
-
/* @__PURE__ */ jsx10(Text6, { color: PALETTE.text, children: s.dictId.padEnd(14) }),
|
|
2062
|
-
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2063
|
-
" ",
|
|
2064
|
-
"ch",
|
|
2065
|
-
String(s.chapter + 1).padStart(3),
|
|
2066
|
-
" ",
|
|
2067
|
-
s.mode.padEnd(9),
|
|
2068
|
-
" ",
|
|
2069
|
-
String(s.wordCount).padStart(3),
|
|
2070
|
-
"w ",
|
|
2071
|
-
s.errors,
|
|
2072
|
-
"err ",
|
|
2073
|
-
computeWPM(s),
|
|
2074
|
-
"wpm ",
|
|
2075
|
-
Math.round(accuracy(s) * 1e3) / 10,
|
|
2076
|
-
"%"
|
|
2077
|
-
] })
|
|
2078
|
-
] }, i))
|
|
3058
|
+
/* @__PURE__ */ jsxs8(Box9, { marginTop: 2, flexDirection: "column", children: [
|
|
3059
|
+
/* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.recent }),
|
|
3060
|
+
recent.map((s, i) => /* @__PURE__ */ jsx14(RecentRow, { session: s, units: t.stats.recentUnits }, i))
|
|
2079
3061
|
] }),
|
|
2080
|
-
top.length > 0 && /* @__PURE__ */
|
|
2081
|
-
/* @__PURE__ */
|
|
2082
|
-
top.map(([word, entry]) => /* @__PURE__ */
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
3062
|
+
top.length > 0 && /* @__PURE__ */ jsxs8(Box9, { marginTop: 2, flexDirection: "column", children: [
|
|
3063
|
+
/* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.topMistakes }),
|
|
3064
|
+
top.map(([word, entry]) => /* @__PURE__ */ jsx14(
|
|
3065
|
+
MistakeRow,
|
|
3066
|
+
{
|
|
3067
|
+
word,
|
|
3068
|
+
count: entry.count,
|
|
3069
|
+
dictIds: entry.dictIds,
|
|
3070
|
+
multiSuffix: t.stats.multiDictSuffix
|
|
3071
|
+
},
|
|
3072
|
+
word
|
|
3073
|
+
))
|
|
3074
|
+
] }),
|
|
3075
|
+
/* @__PURE__ */ jsx14(Box9, { flexGrow: 1 }),
|
|
3076
|
+
/* @__PURE__ */ jsx14(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: t.stats.footer }) })
|
|
3077
|
+
] });
|
|
3078
|
+
}
|
|
3079
|
+
function SparkRow({
|
|
3080
|
+
label,
|
|
3081
|
+
values,
|
|
3082
|
+
maxLabel,
|
|
3083
|
+
suffix = ""
|
|
3084
|
+
}) {
|
|
3085
|
+
const max = values.length === 0 ? 0 : Math.max(...values);
|
|
3086
|
+
const maxDisplay = `${Math.round(max)}${suffix}`;
|
|
3087
|
+
return /* @__PURE__ */ jsxs8(Box9, { children: [
|
|
3088
|
+
/* @__PURE__ */ jsx14(Box9, { width: 10, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: label }) }),
|
|
3089
|
+
/* @__PURE__ */ jsx14(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.accent, children: sparkline(values) }) }),
|
|
3090
|
+
/* @__PURE__ */ jsx14(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
|
|
3091
|
+
maxLabel,
|
|
3092
|
+
" ",
|
|
3093
|
+
maxDisplay
|
|
3094
|
+
] }) })
|
|
3095
|
+
] });
|
|
3096
|
+
}
|
|
3097
|
+
function RecentRow({
|
|
3098
|
+
session,
|
|
3099
|
+
units
|
|
3100
|
+
}) {
|
|
3101
|
+
const name = useDictName(session.dictId);
|
|
3102
|
+
const display = truncateName(name, 14);
|
|
3103
|
+
return /* @__PURE__ */ jsxs8(Box9, { children: [
|
|
3104
|
+
/* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
|
|
3105
|
+
" ",
|
|
3106
|
+
session.ts.replace("T", " ").slice(0, 16),
|
|
3107
|
+
" "
|
|
2091
3108
|
] }),
|
|
2092
|
-
/* @__PURE__ */
|
|
2093
|
-
/* @__PURE__ */
|
|
3109
|
+
/* @__PURE__ */ jsx14(Text8, { color: PALETTE.text, children: display.padEnd(14) }),
|
|
3110
|
+
/* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
|
|
3111
|
+
" ",
|
|
3112
|
+
"ch",
|
|
3113
|
+
String(session.chapter + 1).padStart(3),
|
|
3114
|
+
" ",
|
|
3115
|
+
session.mode.padEnd(9),
|
|
3116
|
+
" ",
|
|
3117
|
+
String(session.wordCount).padStart(3),
|
|
3118
|
+
units.words,
|
|
3119
|
+
" ",
|
|
3120
|
+
session.errors,
|
|
3121
|
+
units.errors,
|
|
3122
|
+
" ",
|
|
3123
|
+
computeWPM(session),
|
|
3124
|
+
units.wpm,
|
|
3125
|
+
" ",
|
|
3126
|
+
Math.round(accuracy(session) * 1e3) / 10,
|
|
3127
|
+
"%"
|
|
3128
|
+
] })
|
|
3129
|
+
] });
|
|
3130
|
+
}
|
|
3131
|
+
function MistakeRow({
|
|
3132
|
+
word,
|
|
3133
|
+
count,
|
|
3134
|
+
dictIds,
|
|
3135
|
+
multiSuffix
|
|
3136
|
+
}) {
|
|
3137
|
+
const firstId = dictIds[0] ?? "";
|
|
3138
|
+
const firstName = useDictName(firstId);
|
|
3139
|
+
const suffix = dictIds.length > 1 ? multiSuffix(dictIds.length - 1) : "";
|
|
3140
|
+
return /* @__PURE__ */ jsxs8(Box9, { children: [
|
|
3141
|
+
/* @__PURE__ */ jsxs8(Text8, { color: PALETTE.error, children: [
|
|
3142
|
+
" ",
|
|
3143
|
+
String(count).padStart(3),
|
|
3144
|
+
" "
|
|
3145
|
+
] }),
|
|
3146
|
+
/* @__PURE__ */ jsx14(Text8, { color: PALETTE.text, children: word.padEnd(20) }),
|
|
3147
|
+
/* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
|
|
3148
|
+
truncateName(firstName, 20),
|
|
3149
|
+
suffix
|
|
3150
|
+
] })
|
|
2094
3151
|
] });
|
|
2095
3152
|
}
|
|
2096
3153
|
function Stat({ label, value, accent = false }) {
|
|
2097
|
-
return /* @__PURE__ */
|
|
2098
|
-
/* @__PURE__ */
|
|
3154
|
+
return /* @__PURE__ */ jsxs8(Box9, { marginRight: 3, children: [
|
|
3155
|
+
/* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
|
|
2099
3156
|
label,
|
|
2100
3157
|
" "
|
|
2101
3158
|
] }),
|
|
2102
|
-
/* @__PURE__ */
|
|
3159
|
+
/* @__PURE__ */ jsx14(Text8, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
|
|
2103
3160
|
] });
|
|
2104
3161
|
}
|
|
2105
3162
|
|
|
2106
3163
|
// src/ui/screens/WordLookup.tsx
|
|
2107
|
-
import { useEffect as
|
|
2108
|
-
import { Box as
|
|
3164
|
+
import { useEffect as useEffect9, useState as useState13 } from "react";
|
|
3165
|
+
import { Box as Box10, Text as Text9, useInput as useInput8 } from "ink";
|
|
2109
3166
|
import { readdir } from "fs/promises";
|
|
2110
|
-
import { Fragment as
|
|
3167
|
+
import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2111
3168
|
async function listLocalDictIds() {
|
|
2112
3169
|
try {
|
|
2113
3170
|
const files = await readdir(paths.dictsDir);
|
|
@@ -2118,12 +3175,13 @@ async function listLocalDictIds() {
|
|
|
2118
3175
|
}
|
|
2119
3176
|
function WordLookup() {
|
|
2120
3177
|
const nav = useNav();
|
|
2121
|
-
const
|
|
2122
|
-
const [
|
|
2123
|
-
const [
|
|
2124
|
-
const [
|
|
2125
|
-
const [
|
|
2126
|
-
|
|
3178
|
+
const t = useStrings();
|
|
3179
|
+
const [query, setQuery] = useState13("");
|
|
3180
|
+
const [allWords, setAllWords] = useState13([]);
|
|
3181
|
+
const [book, setBook] = useState13({});
|
|
3182
|
+
const [loading, setLoading] = useState13(true);
|
|
3183
|
+
const [selected, setSelected] = useState13(0);
|
|
3184
|
+
useEffect9(() => {
|
|
2127
3185
|
void (async () => {
|
|
2128
3186
|
const ids = await listLocalDictIds();
|
|
2129
3187
|
const collected = [];
|
|
@@ -2139,7 +3197,7 @@ function WordLookup() {
|
|
|
2139
3197
|
}, []);
|
|
2140
3198
|
const q = query.toLowerCase().trim();
|
|
2141
3199
|
const filtered = q ? allWords.filter((h) => h.word.name.toLowerCase().includes(q)).slice(0, 50) : [];
|
|
2142
|
-
|
|
3200
|
+
useInput8((input, key) => {
|
|
2143
3201
|
if (key.escape) {
|
|
2144
3202
|
nav.back();
|
|
2145
3203
|
return;
|
|
@@ -2163,115 +3221,223 @@ function WordLookup() {
|
|
|
2163
3221
|
}
|
|
2164
3222
|
});
|
|
2165
3223
|
if (loading) {
|
|
2166
|
-
return /* @__PURE__ */
|
|
3224
|
+
return /* @__PURE__ */ jsx15(Box10, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.indexing }) });
|
|
2167
3225
|
}
|
|
2168
3226
|
if (allWords.length === 0) {
|
|
2169
|
-
return /* @__PURE__ */
|
|
2170
|
-
/* @__PURE__ */
|
|
2171
|
-
/* @__PURE__ */
|
|
2172
|
-
/* @__PURE__ */
|
|
3227
|
+
return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
3228
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.none }),
|
|
3229
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.pullFirst }) }),
|
|
3230
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 2, children: /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.muted, children: [
|
|
3231
|
+
"[Esc] ",
|
|
3232
|
+
t.common.back
|
|
3233
|
+
] }) })
|
|
2173
3234
|
] });
|
|
2174
3235
|
}
|
|
2175
3236
|
const current = filtered[selected];
|
|
2176
|
-
return /* @__PURE__ */
|
|
2177
|
-
/* @__PURE__ */
|
|
2178
|
-
/* @__PURE__ */
|
|
2179
|
-
/* @__PURE__ */
|
|
2180
|
-
/* @__PURE__ */
|
|
2181
|
-
allWords.length,
|
|
2182
|
-
" words across local dicts"
|
|
2183
|
-
] })
|
|
3237
|
+
return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
3238
|
+
/* @__PURE__ */ jsxs9(Box10, { children: [
|
|
3239
|
+
/* @__PURE__ */ jsx15(Text9, { bold: true, color: PALETTE.accent, children: t.word.title }),
|
|
3240
|
+
/* @__PURE__ */ jsx15(Box10, { flexGrow: 1 }),
|
|
3241
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.countAcross(allWords.length) })
|
|
2184
3242
|
] }),
|
|
2185
|
-
/* @__PURE__ */
|
|
2186
|
-
/* @__PURE__ */
|
|
2187
|
-
/* @__PURE__ */
|
|
2188
|
-
/* @__PURE__ */
|
|
3243
|
+
/* @__PURE__ */ jsxs9(Box10, { marginTop: 1, children: [
|
|
3244
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: "> " }),
|
|
3245
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.text, children: query }),
|
|
3246
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.accent, children: "_" })
|
|
2189
3247
|
] }),
|
|
2190
|
-
/* @__PURE__ */
|
|
2191
|
-
/* @__PURE__ */
|
|
2192
|
-
filtered.map((h, i) => {
|
|
2193
|
-
|
|
2194
|
-
return /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
2195
|
-
/* @__PURE__ */ jsx11(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2196
|
-
/* @__PURE__ */ jsx11(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: h.word.name.padEnd(20) }),
|
|
2197
|
-
/* @__PURE__ */ jsx11(Text7, { color: PALETTE.muted, children: h.dictId })
|
|
2198
|
-
] }, `${h.dictId}-${h.word.name}-${i}`);
|
|
2199
|
-
}),
|
|
2200
|
-
filtered.length === 0 && q && /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.muted, children: [
|
|
2201
|
-
'no matches for "',
|
|
2202
|
-
query,
|
|
2203
|
-
'"'
|
|
2204
|
-
] })
|
|
3248
|
+
/* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexGrow: 1, children: [
|
|
3249
|
+
/* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", width: "40%", children: [
|
|
3250
|
+
filtered.map((h, i) => /* @__PURE__ */ jsx15(HitRow, { hit: h, active: i === selected }, `${h.dictId}-${h.word.name}-${i}`)),
|
|
3251
|
+
filtered.length === 0 && q && /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.noMatches(query) })
|
|
2205
3252
|
] }),
|
|
2206
|
-
/* @__PURE__ */
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
"(last ",
|
|
2236
|
-
book[current.word.name].lastSeen.slice(0, 10),
|
|
2237
|
-
")"
|
|
2238
|
-
] })
|
|
2239
|
-
] })
|
|
2240
|
-
] }) })
|
|
3253
|
+
/* @__PURE__ */ jsx15(Box10, { flexDirection: "column", width: "60%", paddingLeft: 2, children: current && /* @__PURE__ */ jsx15(Detail, { hit: current, book }) })
|
|
3254
|
+
] }),
|
|
3255
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.footer }) })
|
|
3256
|
+
] });
|
|
3257
|
+
}
|
|
3258
|
+
function HitRow({ hit, active }) {
|
|
3259
|
+
const name = useDictName(hit.dictId);
|
|
3260
|
+
return /* @__PURE__ */ jsxs9(Box10, { children: [
|
|
3261
|
+
/* @__PURE__ */ jsx15(Text9, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
3262
|
+
/* @__PURE__ */ jsx15(Text9, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: hit.word.name.padEnd(20) }),
|
|
3263
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: truncateName(name, 18) })
|
|
3264
|
+
] });
|
|
3265
|
+
}
|
|
3266
|
+
function Detail({ hit, book }) {
|
|
3267
|
+
const t = useStrings();
|
|
3268
|
+
const name = useDictName(hit.dictId);
|
|
3269
|
+
return /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
3270
|
+
/* @__PURE__ */ jsx15(Text9, { bold: true, color: PALETTE.text, children: hit.word.name }),
|
|
3271
|
+
/* @__PURE__ */ jsxs9(Box10, { marginTop: 1, children: [
|
|
3272
|
+
hit.word.usphone && /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.muted, children: [
|
|
3273
|
+
"US /",
|
|
3274
|
+
hit.word.usphone,
|
|
3275
|
+
"/ "
|
|
3276
|
+
] }),
|
|
3277
|
+
hit.word.ukphone && /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.muted, children: [
|
|
3278
|
+
"UK /",
|
|
3279
|
+
hit.word.ukphone,
|
|
3280
|
+
"/"
|
|
3281
|
+
] })
|
|
2241
3282
|
] }),
|
|
2242
|
-
/* @__PURE__ */
|
|
3283
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, flexDirection: "column", children: (hit.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs9(Text9, { color: PALETTE.primary, children: [
|
|
3284
|
+
"\xB7 ",
|
|
3285
|
+
tr
|
|
3286
|
+
] }, i)) }),
|
|
3287
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.word.inDict(truncateName(name, 22)) }) }),
|
|
3288
|
+
book[hit.word.name] && /* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.error, children: t.word.mistakes(book[hit.word.name].count, book[hit.word.name].lastSeen.slice(0, 10)) }) })
|
|
3289
|
+
] });
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
// src/ui/screens/HelpScreen.tsx
|
|
3293
|
+
import { Box as Box11, Text as Text10, useInput as useInput9 } from "ink";
|
|
3294
|
+
import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3295
|
+
function HelpScreen() {
|
|
3296
|
+
const nav = useNav();
|
|
3297
|
+
const t = useStrings();
|
|
3298
|
+
useInput9((_input, key) => {
|
|
3299
|
+
if (key.escape) nav.back();
|
|
3300
|
+
});
|
|
3301
|
+
const k = t.help.keys;
|
|
3302
|
+
const sections = [
|
|
3303
|
+
{ title: t.help.sections.global, keys: [k.helpScreen, k.quit] },
|
|
3304
|
+
{ title: t.help.sections.main, keys: [k.navigate, k.select, k.letterJump, k.helpScreen] },
|
|
3305
|
+
{
|
|
3306
|
+
title: t.help.sections.practice,
|
|
3307
|
+
keys: [k.pause, k.skip, k.replay, k.resume, k.nextChapter, k.reviewMistakes, k.stealthToggle, k.backMenu]
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
title: t.help.sections.dict,
|
|
3311
|
+
keys: [k.navigate, k.filter, k.itemActions, k.moreActions, k.backScreen]
|
|
3312
|
+
},
|
|
3313
|
+
{ title: t.help.sections.config, keys: [k.navigate, k.select, k.backMenu] },
|
|
3314
|
+
{ title: t.help.sections.stats, keys: [k.cycleWindow, k.backMenu] },
|
|
3315
|
+
{ title: t.help.sections.word, keys: [k.filter, k.navigate, k.backMenu] }
|
|
3316
|
+
];
|
|
3317
|
+
return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
3318
|
+
/* @__PURE__ */ jsxs10(Box11, { children: [
|
|
3319
|
+
/* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.accent, children: t.help.title }),
|
|
3320
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: " \xB7 " }),
|
|
3321
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.help.subtitle })
|
|
3322
|
+
] }),
|
|
3323
|
+
/* @__PURE__ */ jsx16(Box11, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: sections.map((sec) => /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
|
|
3324
|
+
/* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.text, children: sec.title }),
|
|
3325
|
+
sec.keys.map((line, i) => /* @__PURE__ */ jsx16(Box11, { children: /* @__PURE__ */ jsxs10(Text10, { color: PALETTE.muted, children: [
|
|
3326
|
+
" \xB7 ",
|
|
3327
|
+
line
|
|
3328
|
+
] }) }, i))
|
|
3329
|
+
] }, sec.title)) }),
|
|
3330
|
+
/* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.help.footer }) })
|
|
2243
3331
|
] });
|
|
2244
3332
|
}
|
|
2245
3333
|
|
|
2246
3334
|
// src/ui/App.tsx
|
|
2247
|
-
import { jsx as
|
|
3335
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2248
3336
|
function App({ initial, initialCfg }) {
|
|
2249
|
-
return /* @__PURE__ */
|
|
3337
|
+
return /* @__PURE__ */ jsx17(AppStateProvider, { initialCfg, children: /* @__PURE__ */ jsx17(LangBridge, { children: /* @__PURE__ */ jsx17(RegistryProvider, { children: /* @__PURE__ */ jsx17(AudioStatusProvider, { disabled: !initialCfg.sounds.master, children: /* @__PURE__ */ jsx17(NavProvider, { initial, children: /* @__PURE__ */ jsx17(Fullscreen, { children: /* @__PURE__ */ jsx17(Router, {}) }) }) }) }) }) });
|
|
3338
|
+
}
|
|
3339
|
+
function LangBridge({ children }) {
|
|
3340
|
+
const { cfg } = useAppState();
|
|
3341
|
+
return /* @__PURE__ */ jsx17(StringsProvider, { pref: cfg.language, children });
|
|
3342
|
+
}
|
|
3343
|
+
function screenKey(frame) {
|
|
3344
|
+
if (frame.name === "practice") {
|
|
3345
|
+
const p = frame.params;
|
|
3346
|
+
return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? "s" : "n"}`;
|
|
3347
|
+
}
|
|
3348
|
+
return frame.name;
|
|
2250
3349
|
}
|
|
2251
3350
|
function Router() {
|
|
2252
3351
|
const nav = useNav();
|
|
2253
3352
|
const { cfg } = useAppState();
|
|
2254
3353
|
const { exit } = useApp4();
|
|
2255
|
-
|
|
2256
|
-
|
|
3354
|
+
const lastKeyRef = useRef4(null);
|
|
3355
|
+
useInput10((input, key2) => {
|
|
3356
|
+
if (key2.ctrl && input === "c") exit();
|
|
2257
3357
|
});
|
|
2258
3358
|
const frame = nav.current;
|
|
3359
|
+
const key = screenKey(frame);
|
|
3360
|
+
if (lastKeyRef.current !== key) {
|
|
3361
|
+
if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[H");
|
|
3362
|
+
lastKeyRef.current = key;
|
|
3363
|
+
}
|
|
2259
3364
|
switch (frame.name) {
|
|
2260
3365
|
case "main":
|
|
2261
|
-
return /* @__PURE__ */
|
|
3366
|
+
return /* @__PURE__ */ jsx17(MainMenu, { cfg });
|
|
2262
3367
|
case "practice":
|
|
2263
|
-
return /* @__PURE__ */
|
|
3368
|
+
return /* @__PURE__ */ jsx17(PracticeScreen, { params: frame.params });
|
|
2264
3369
|
case "dict":
|
|
2265
|
-
return /* @__PURE__ */
|
|
3370
|
+
return /* @__PURE__ */ jsx17(DictBrowser, { params: frame.params });
|
|
2266
3371
|
case "config":
|
|
2267
|
-
return /* @__PURE__ */
|
|
3372
|
+
return /* @__PURE__ */ jsx17(ConfigEditor, {});
|
|
2268
3373
|
case "stats":
|
|
2269
|
-
return /* @__PURE__ */
|
|
3374
|
+
return /* @__PURE__ */ jsx17(StatsViewer, {});
|
|
2270
3375
|
case "word":
|
|
2271
|
-
return /* @__PURE__ */
|
|
3376
|
+
return /* @__PURE__ */ jsx17(WordLookup, {});
|
|
3377
|
+
case "help":
|
|
3378
|
+
return /* @__PURE__ */ jsx17(HelpScreen, {});
|
|
2272
3379
|
}
|
|
2273
3380
|
}
|
|
2274
3381
|
|
|
3382
|
+
// src/util/report.ts
|
|
3383
|
+
import chalk3 from "chalk";
|
|
3384
|
+
var LEAVE_ALTSCREEN = "\x1B[?25h\x1B[?1049l";
|
|
3385
|
+
function ensureMainScreen() {
|
|
3386
|
+
if (process.stdout.isTTY) process.stdout.write(LEAVE_ALTSCREEN);
|
|
3387
|
+
}
|
|
3388
|
+
function fmtDuration(ms, lang) {
|
|
3389
|
+
const total = Math.floor(ms / 1e3);
|
|
3390
|
+
const m = Math.floor(total / 60);
|
|
3391
|
+
const s = total % 60;
|
|
3392
|
+
if (lang === "zh") {
|
|
3393
|
+
if (m === 0) return `${s} \u79D2`;
|
|
3394
|
+
return `${m} \u5206 ${s} \u79D2`;
|
|
3395
|
+
}
|
|
3396
|
+
if (m === 0) return `${s}s`;
|
|
3397
|
+
return `${m}m ${s}s`;
|
|
3398
|
+
}
|
|
3399
|
+
function printSessionReport(r, t, lang) {
|
|
3400
|
+
if (r.startedAt === null && r.chaptersCompleted === 0) return;
|
|
3401
|
+
if (r.chaptersCompleted === 0) {
|
|
3402
|
+
console.log();
|
|
3403
|
+
console.log(chalk3.bold.cyan(t.report.title));
|
|
3404
|
+
const labelW2 = Math.max(visibleWidth2(t.report.duration), visibleWidth2(t.report.notPracticed)) + 2;
|
|
3405
|
+
const pad2 = (label) => label + " ".repeat(Math.max(0, labelW2 - visibleWidth2(label)));
|
|
3406
|
+
console.log(` ${chalk3.dim(pad2(t.report.duration))} ${fmtDuration(r.totalDurationMs, lang)}`);
|
|
3407
|
+
console.log(` ${chalk3.dim(t.report.notPracticed)}`);
|
|
3408
|
+
console.log();
|
|
3409
|
+
console.log(chalk3.dim(` ${t.report.farewell}`));
|
|
3410
|
+
console.log();
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
const accPct = Math.round(r.accuracy * 1e3) / 10;
|
|
3414
|
+
const labels = [
|
|
3415
|
+
t.report.duration,
|
|
3416
|
+
t.report.practiced,
|
|
3417
|
+
t.report.chapters,
|
|
3418
|
+
t.report.words,
|
|
3419
|
+
t.report.accuracy,
|
|
3420
|
+
t.report.wpm
|
|
3421
|
+
];
|
|
3422
|
+
if (r.newMistakeWords > 0) labels.push(t.report.newMistakes);
|
|
3423
|
+
const labelW = Math.max(...labels.map(visibleWidth2)) + 2;
|
|
3424
|
+
const pad = (label) => label + " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
|
|
3425
|
+
console.log();
|
|
3426
|
+
console.log(chalk3.bold.cyan(t.report.title));
|
|
3427
|
+
console.log(` ${chalk3.dim(pad(t.report.duration))} ${fmtDuration(r.totalDurationMs, lang)}`);
|
|
3428
|
+
console.log(` ${chalk3.dim(pad(t.report.practiced))} ${fmtDuration(r.practiceMs, lang)}`);
|
|
3429
|
+
console.log(` ${chalk3.dim(pad(t.report.chapters))} ${r.chaptersCompleted}`);
|
|
3430
|
+
console.log(` ${chalk3.dim(pad(t.report.words))} ${r.wordCount}`);
|
|
3431
|
+
console.log(` ${chalk3.dim(pad(t.report.accuracy))} ${accPct}%`);
|
|
3432
|
+
console.log(` ${chalk3.dim(pad(t.report.wpm))} ${r.wpm}`);
|
|
3433
|
+
if (r.newMistakeWords > 0) {
|
|
3434
|
+
console.log(` ${chalk3.dim(pad(t.report.newMistakes))} ${r.newMistakeWords}`);
|
|
3435
|
+
}
|
|
3436
|
+
console.log();
|
|
3437
|
+
console.log(chalk3.dim(` ${t.report.farewell}`));
|
|
3438
|
+
console.log();
|
|
3439
|
+
}
|
|
3440
|
+
|
|
2275
3441
|
// src/commands/practice.ts
|
|
2276
3442
|
var MODES = ["order", "dictation", "review", "random", "loop"];
|
|
2277
3443
|
function isMode(v) {
|
|
@@ -2279,42 +3445,49 @@ function isMode(v) {
|
|
|
2279
3445
|
}
|
|
2280
3446
|
async function runPractice(dictIdArg, options) {
|
|
2281
3447
|
if (!process.stdout.isTTY) {
|
|
2282
|
-
console.error(
|
|
3448
|
+
console.error(chalk4.red("Practice requires an interactive TTY."));
|
|
2283
3449
|
process.exitCode = 1;
|
|
2284
3450
|
return;
|
|
2285
3451
|
}
|
|
2286
3452
|
const cfg = await loadConfig();
|
|
2287
3453
|
const dictId = dictIdArg ?? cfg.defaultDict;
|
|
2288
3454
|
if (!dictId) {
|
|
2289
|
-
console.error(
|
|
3455
|
+
console.error(chalk4.red("No dictionary specified. Pass an id or set config.defaultDict."));
|
|
2290
3456
|
process.exitCode = 1;
|
|
2291
3457
|
return;
|
|
2292
3458
|
}
|
|
2293
3459
|
const mode = options.mode ?? cfg.defaultMode;
|
|
2294
3460
|
if (!isMode(mode)) {
|
|
2295
|
-
console.error(
|
|
3461
|
+
console.error(chalk4.red(`Invalid mode "${mode}". Valid: ${MODES.join(", ")}`));
|
|
2296
3462
|
process.exitCode = 1;
|
|
2297
3463
|
return;
|
|
2298
3464
|
}
|
|
2299
3465
|
const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);
|
|
3466
|
+
const stealth = options.stealth === true || cfg.stealth === "default";
|
|
3467
|
+
start();
|
|
2300
3468
|
const { waitUntilExit } = render(
|
|
2301
3469
|
createElement(App, {
|
|
2302
|
-
initial: { name: "practice", params: { dictId, chapterIndex, mode } },
|
|
3470
|
+
initial: { name: "practice", params: { dictId, chapterIndex, mode, stealth } },
|
|
2303
3471
|
initialCfg: cfg
|
|
2304
3472
|
}),
|
|
2305
3473
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
2306
3474
|
);
|
|
2307
3475
|
await waitUntilExit();
|
|
3476
|
+
ensureMainScreen();
|
|
3477
|
+
const { lang, t } = pickStrings(cfg.language);
|
|
3478
|
+
printSessionReport(report(), t, lang);
|
|
2308
3479
|
}
|
|
2309
3480
|
function buildPracticeCommand() {
|
|
2310
|
-
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").
|
|
2311
|
-
|
|
2312
|
-
|
|
3481
|
+
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").option("--stealth", "enter stealth mode (minimal UI, no sound)").action(
|
|
3482
|
+
async (dictIdArg, options) => {
|
|
3483
|
+
await runPractice(dictIdArg, options);
|
|
3484
|
+
}
|
|
3485
|
+
);
|
|
2313
3486
|
}
|
|
2314
3487
|
|
|
2315
3488
|
// src/commands/stats.ts
|
|
2316
3489
|
import { Command as Command4 } from "commander";
|
|
2317
|
-
import
|
|
3490
|
+
import chalk5 from "chalk";
|
|
2318
3491
|
function buildStatsCommand() {
|
|
2319
3492
|
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
3493
|
const days = Math.max(1, Number(opts.days) || 14);
|
|
@@ -2322,7 +3495,7 @@ function buildStatsCommand() {
|
|
|
2322
3495
|
const sessions = await loadSessions();
|
|
2323
3496
|
const book = await loadMistakes();
|
|
2324
3497
|
if (sessions.length === 0) {
|
|
2325
|
-
console.log(
|
|
3498
|
+
console.log(chalk5.yellow("No practice history yet. Run `qwerty practice <dict>` to get started."));
|
|
2326
3499
|
return;
|
|
2327
3500
|
}
|
|
2328
3501
|
const buckets = dailyBuckets(sessions, days);
|
|
@@ -2336,33 +3509,33 @@ function buildStatsCommand() {
|
|
|
2336
3509
|
);
|
|
2337
3510
|
const overallWpm = totalMs > 0 ? Math.round(totalWords / (totalMs / 6e4) * 10) / 10 : 0;
|
|
2338
3511
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
2339
|
-
console.log(
|
|
2340
|
-
console.log(` ${
|
|
2341
|
-
console.log(` ${
|
|
2342
|
-
console.log(
|
|
3512
|
+
console.log(chalk5.bold("\nLifetime"));
|
|
3513
|
+
console.log(` ${chalk5.dim("sessions")} ${sessions.length} ${chalk5.dim("words")} ${totalWords} ${chalk5.dim("errors")} ${totalErrors}`);
|
|
3514
|
+
console.log(` ${chalk5.dim("avg wpm")} ${overallWpm} ${chalk5.dim("avg accuracy")} ${Math.round(overallAcc * 1e3) / 10}% ${chalk5.dim("streak")} ${chalk5.bold(streak)}d`);
|
|
3515
|
+
console.log(chalk5.bold(`
|
|
2343
3516
|
Last ${days} days`));
|
|
2344
|
-
console.log(` ${
|
|
2345
|
-
console.log(` ${
|
|
2346
|
-
console.log(` ${
|
|
3517
|
+
console.log(` ${chalk5.dim("wpm ")} ${sparkline(buckets.map((b) => b.wpm))} ${chalk5.dim("max")} ${Math.round(Math.max(...buckets.map((b) => b.wpm)))}`);
|
|
3518
|
+
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)))}%`);
|
|
3519
|
+
console.log(` ${chalk5.dim("sessions")} ${sparkline(buckets.map((b) => b.sessions))}`);
|
|
2347
3520
|
const recent = sessions.slice(-5).reverse();
|
|
2348
|
-
console.log(
|
|
3521
|
+
console.log(chalk5.bold("\nLast 5 sessions"));
|
|
2349
3522
|
for (const s of recent) {
|
|
2350
3523
|
const wpm = computeWPM(s);
|
|
2351
3524
|
const acc = Math.round(accuracy(s) * 1e3) / 10;
|
|
2352
3525
|
console.log(
|
|
2353
|
-
` ${
|
|
3526
|
+
` ${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
3527
|
);
|
|
2355
3528
|
}
|
|
2356
3529
|
const top = topN(book, topCount);
|
|
2357
3530
|
if (top.length > 0) {
|
|
2358
|
-
console.log(
|
|
3531
|
+
console.log(chalk5.bold(`
|
|
2359
3532
|
Top ${top.length} mistakes`));
|
|
2360
3533
|
for (const [word, entry] of top) {
|
|
2361
|
-
console.log(` ${
|
|
3534
|
+
console.log(` ${chalk5.red(String(entry.count).padStart(3))} ${chalk5.bold(word.padEnd(20))} ${chalk5.dim(entry.dictIds.join(", "))}`);
|
|
2362
3535
|
}
|
|
2363
3536
|
} else {
|
|
2364
|
-
console.log(
|
|
2365
|
-
console.log(
|
|
3537
|
+
console.log(chalk5.bold("\nTop mistakes"));
|
|
3538
|
+
console.log(chalk5.dim(" none \u2014 keep going"));
|
|
2366
3539
|
}
|
|
2367
3540
|
console.log();
|
|
2368
3541
|
});
|
|
@@ -2370,7 +3543,7 @@ Top ${top.length} mistakes`));
|
|
|
2370
3543
|
|
|
2371
3544
|
// src/commands/word.ts
|
|
2372
3545
|
import { Command as Command5 } from "commander";
|
|
2373
|
-
import
|
|
3546
|
+
import chalk6 from "chalk";
|
|
2374
3547
|
import { readdir as readdir2 } from "fs/promises";
|
|
2375
3548
|
async function listLocalDictIds2() {
|
|
2376
3549
|
try {
|
|
@@ -2385,7 +3558,7 @@ function buildWordCommand() {
|
|
|
2385
3558
|
const q = keyword.toLowerCase();
|
|
2386
3559
|
const ids = await listLocalDictIds2();
|
|
2387
3560
|
if (ids.length === 0) {
|
|
2388
|
-
console.log(
|
|
3561
|
+
console.log(chalk6.yellow("No local dictionaries. Run `qwerty dict pull <id>` first."));
|
|
2389
3562
|
return;
|
|
2390
3563
|
}
|
|
2391
3564
|
const hits = [];
|
|
@@ -2399,7 +3572,7 @@ function buildWordCommand() {
|
|
|
2399
3572
|
}
|
|
2400
3573
|
}
|
|
2401
3574
|
if (hits.length === 0) {
|
|
2402
|
-
console.log(
|
|
3575
|
+
console.log(chalk6.yellow(`No matches for "${keyword}" in ${ids.length} local dictionaries`));
|
|
2403
3576
|
return;
|
|
2404
3577
|
}
|
|
2405
3578
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2412,27 +3585,37 @@ function buildWordCommand() {
|
|
|
2412
3585
|
for (const [name, group] of byName) {
|
|
2413
3586
|
const first = group[0].word;
|
|
2414
3587
|
console.log();
|
|
2415
|
-
console.log(
|
|
3588
|
+
console.log(chalk6.bold.white(name));
|
|
2416
3589
|
const us = first.usphone ? `US /${first.usphone}/` : "";
|
|
2417
3590
|
const uk = first.ukphone ? `UK /${first.ukphone}/` : "";
|
|
2418
|
-
if (us || uk) console.log(
|
|
2419
|
-
for (const t of first.trans ?? []) console.log(
|
|
3591
|
+
if (us || uk) console.log(chalk6.dim(` ${[us, uk].filter(Boolean).join(" ")}`));
|
|
3592
|
+
for (const t of first.trans ?? []) console.log(chalk6.cyan(` \xB7 ${t}`));
|
|
2420
3593
|
const sources = await Promise.all(
|
|
2421
3594
|
group.map(async (h) => {
|
|
2422
3595
|
const reg = await findEntry(h.dictId);
|
|
2423
3596
|
return reg?.name ?? h.dictId;
|
|
2424
3597
|
})
|
|
2425
3598
|
);
|
|
2426
|
-
console.log(
|
|
3599
|
+
console.log(chalk6.dim(` in: ${sources.join(", ")}`));
|
|
2427
3600
|
const mistake = book[name];
|
|
2428
3601
|
if (mistake) {
|
|
2429
|
-
console.log(
|
|
3602
|
+
console.log(chalk6.dim(` mistakes: ${mistake.count} (last ${mistake.lastSeen.slice(0, 10)})`));
|
|
2430
3603
|
}
|
|
2431
3604
|
}
|
|
2432
3605
|
console.log();
|
|
2433
3606
|
});
|
|
2434
3607
|
}
|
|
2435
3608
|
|
|
3609
|
+
// src/commands/stealth.ts
|
|
3610
|
+
import { Command as Command6 } from "commander";
|
|
3611
|
+
function buildStealthCommand() {
|
|
3612
|
+
return new Command6("boss").alias("stealth").description("Start practice in stealth mode (minimal UI, looks like plain terminal output)").argument("[dictId]", "dictionary id; falls back to config.defaultDict").option("-c, --chapter <n>", "chapter number (1-based)", "1").option("-m, --mode <mode>", "order | dictation | review | random | loop").action(
|
|
3613
|
+
async (dictIdArg, options) => {
|
|
3614
|
+
await runPractice(dictIdArg, { ...options, stealth: true });
|
|
3615
|
+
}
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
|
|
2436
3619
|
// src/commands/menu.ts
|
|
2437
3620
|
import { render as render2 } from "ink";
|
|
2438
3621
|
import { createElement as createElement2 } from "react";
|
|
@@ -2442,17 +3625,22 @@ async function runMainMenu() {
|
|
|
2442
3625
|
return;
|
|
2443
3626
|
}
|
|
2444
3627
|
const cfg = await loadConfig();
|
|
3628
|
+
start();
|
|
2445
3629
|
const { waitUntilExit } = render2(
|
|
2446
3630
|
createElement2(App, { initial: { name: "main" }, initialCfg: cfg }),
|
|
2447
3631
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
2448
3632
|
);
|
|
2449
3633
|
await waitUntilExit();
|
|
3634
|
+
ensureMainScreen();
|
|
3635
|
+
const { lang, t } = pickStrings(cfg.language);
|
|
3636
|
+
printSessionReport(report(), t, lang);
|
|
2450
3637
|
}
|
|
2451
3638
|
|
|
2452
3639
|
// src/cli.ts
|
|
2453
|
-
var program = new
|
|
3640
|
+
var program = new Command7();
|
|
2454
3641
|
program.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version(package_default.version);
|
|
2455
3642
|
program.addCommand(buildPracticeCommand());
|
|
3643
|
+
program.addCommand(buildStealthCommand());
|
|
2456
3644
|
program.addCommand(buildDictCommand());
|
|
2457
3645
|
program.addCommand(buildWordCommand());
|
|
2458
3646
|
program.addCommand(buildStatsCommand());
|