qwerty-cli 0.0.1-alpha.6 → 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 +1167 -460
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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: {
|
|
@@ -150,7 +150,8 @@ var ConfigSchema = z.object({
|
|
|
150
150
|
autoplayPronunciation: z.boolean().default(true),
|
|
151
151
|
defaultMode: z.enum(["order", "dictation", "review", "random", "loop"]).default("order"),
|
|
152
152
|
defaultDict: z.string().optional(),
|
|
153
|
-
language: z.enum(["auto", "zh", "en"]).default("auto")
|
|
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() {
|
|
@@ -512,7 +513,7 @@ import { createElement } from "react";
|
|
|
512
513
|
|
|
513
514
|
// src/ui/App.tsx
|
|
514
515
|
import { useRef as useRef4 } from "react";
|
|
515
|
-
import { useApp as useApp4, useInput as
|
|
516
|
+
import { useApp as useApp4, useInput as useInput10 } from "ink";
|
|
516
517
|
|
|
517
518
|
// src/ui/nav.tsx
|
|
518
519
|
import { createContext, useContext, useState, useCallback } from "react";
|
|
@@ -804,12 +805,13 @@ var en = {
|
|
|
804
805
|
back: "back",
|
|
805
806
|
quit: "quit",
|
|
806
807
|
on: "on",
|
|
807
|
-
off: "off"
|
|
808
|
+
off: "off",
|
|
809
|
+
cancel: "cancel"
|
|
808
810
|
},
|
|
809
811
|
mainMenu: {
|
|
810
812
|
items: {
|
|
811
813
|
practiceLabel: "Practice",
|
|
812
|
-
practiceHintWith: (
|
|
814
|
+
practiceHintWith: (name) => `start ${name}`,
|
|
813
815
|
practiceHintNone: "pick a dictionary",
|
|
814
816
|
dictLabel: "Dictionaries",
|
|
815
817
|
dictHint: "browse, pull, set default",
|
|
@@ -819,18 +821,19 @@ var en = {
|
|
|
819
821
|
statsHint: "history & trends",
|
|
820
822
|
configLabel: "Config",
|
|
821
823
|
configHint: "edit preferences",
|
|
824
|
+
stealthLabel: "Stealth",
|
|
825
|
+
stealthHint: "quiet practice mode",
|
|
822
826
|
quitLabel: "Quit",
|
|
823
|
-
quitHint: "Ctrl+C also exits"
|
|
827
|
+
quitHint: "Esc or Ctrl+C also exits"
|
|
824
828
|
},
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
hint: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump"
|
|
829
|
+
hint: "\u2191/\u2193 navigate \xB7 Enter select \xB7 letters jump",
|
|
830
|
+
helpHint: "? help"
|
|
828
831
|
},
|
|
829
832
|
dict: {
|
|
830
833
|
title: "Dictionaries",
|
|
831
834
|
loading: "loading dictionaries\u2026",
|
|
832
835
|
entries: (n) => `${n} entries`,
|
|
833
|
-
|
|
836
|
+
filterPlaceholder: "type to filter",
|
|
834
837
|
local: "local \u2713",
|
|
835
838
|
notLocal: "not local",
|
|
836
839
|
defaultMark: "default \u2605",
|
|
@@ -839,7 +842,19 @@ var en = {
|
|
|
839
842
|
pulling: (id) => `pulling ${id}\u2026`,
|
|
840
843
|
removing: (id) => `removing ${id}\u2026`,
|
|
841
844
|
errorOn: (id, msg) => `error on ${id}: ${msg}`,
|
|
842
|
-
footer: "\u2191/\u2193 select \xB7 Enter
|
|
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
|
+
}
|
|
843
858
|
},
|
|
844
859
|
config: {
|
|
845
860
|
title: "Config",
|
|
@@ -847,14 +862,18 @@ var en = {
|
|
|
847
862
|
defaultDict: "default dict",
|
|
848
863
|
defaultMode: "default mode",
|
|
849
864
|
accent: "accent",
|
|
850
|
-
mirror: "mirror",
|
|
865
|
+
mirror: "dict mirror",
|
|
851
866
|
chapterSize: "chapter size",
|
|
852
867
|
autoplayPronunciation: "autoplay pronunciation",
|
|
853
868
|
soundsMaster: "sounds master",
|
|
854
869
|
soundsKeystroke: "sounds keystroke",
|
|
855
870
|
soundsFeedback: "sounds feedback",
|
|
856
871
|
soundsKeySound: "sounds key sound",
|
|
857
|
-
language: "language"
|
|
872
|
+
language: "language",
|
|
873
|
+
stealth: "stealth mode"
|
|
874
|
+
},
|
|
875
|
+
enumValues: {
|
|
876
|
+
stealth: { off: "off", menu: "show in menu", default: "default practice" }
|
|
858
877
|
},
|
|
859
878
|
hints: {
|
|
860
879
|
editing: "type to edit \xB7 Enter save \xB7 Esc cancel",
|
|
@@ -865,7 +884,7 @@ var en = {
|
|
|
865
884
|
}
|
|
866
885
|
},
|
|
867
886
|
stats: {
|
|
868
|
-
title: "Stats",
|
|
887
|
+
title: "Stats \xB7 overview",
|
|
869
888
|
loading: "loading stats\u2026",
|
|
870
889
|
none: "No practice history yet.",
|
|
871
890
|
nonePractice: "Run a practice session first.",
|
|
@@ -876,11 +895,14 @@ var en = {
|
|
|
876
895
|
wpm: "wpm",
|
|
877
896
|
accuracy: "accuracy",
|
|
878
897
|
streak: "streak",
|
|
879
|
-
last: (n) => `last ${n} days (
|
|
880
|
-
cycleWindow: "
|
|
898
|
+
last: (n) => `last ${n} days (\u2190/\u2192 cycle window)`,
|
|
899
|
+
cycleWindow: "\u2190/\u2192 cycle window \xB7 Esc back",
|
|
881
900
|
recent: "recent sessions",
|
|
882
901
|
topMistakes: "top mistakes",
|
|
883
|
-
footer: "
|
|
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`
|
|
884
906
|
},
|
|
885
907
|
word: {
|
|
886
908
|
title: "Word lookup",
|
|
@@ -889,7 +911,7 @@ var en = {
|
|
|
889
911
|
pullFirst: "Pull one in Dictionaries first.",
|
|
890
912
|
countAcross: (n) => `${n} words across local dicts`,
|
|
891
913
|
noMatches: (q) => `no matches for "${q}"`,
|
|
892
|
-
inDict: (
|
|
914
|
+
inDict: (name) => `in: ${name}`,
|
|
893
915
|
mistakes: (n, date) => `mistakes: ${n} (last ${date})`,
|
|
894
916
|
footer: "type to filter \xB7 \u2191/\u2193 select \xB7 Esc back"
|
|
895
917
|
},
|
|
@@ -921,10 +943,20 @@ var en = {
|
|
|
921
943
|
accuracy: "accuracy",
|
|
922
944
|
elapsed: (t) => `elapsed ${t}`
|
|
923
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
|
+
},
|
|
924
958
|
footers: {
|
|
925
|
-
typing: "Ctrl+N skip \xB7 Esc pause \xB7 Tab replay
|
|
926
|
-
paused: "[r] resume \xB7 [q] quit to menu",
|
|
927
|
-
summary: "[n] next chapter \xB7 [m] review mistakes \xB7 [q] back to menu"
|
|
959
|
+
typing: "Ctrl+N skip \xB7 Esc pause \xB7 Tab replay"
|
|
928
960
|
},
|
|
929
961
|
errors: {
|
|
930
962
|
noMistakes: "No mistakes to review yet. Practice some chapters first.",
|
|
@@ -944,7 +976,49 @@ var en = {
|
|
|
944
976
|
accuracy: "accuracy",
|
|
945
977
|
wpm: "wpm",
|
|
946
978
|
newMistakes: "new mistakes",
|
|
947
|
-
farewell: "see you next time."
|
|
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}%`
|
|
948
1022
|
}
|
|
949
1023
|
};
|
|
950
1024
|
var zh = {
|
|
@@ -956,12 +1030,13 @@ var zh = {
|
|
|
956
1030
|
back: "\u8FD4\u56DE",
|
|
957
1031
|
quit: "\u9000\u51FA",
|
|
958
1032
|
on: "\u5F00",
|
|
959
|
-
off: "\u5173"
|
|
1033
|
+
off: "\u5173",
|
|
1034
|
+
cancel: "\u53D6\u6D88"
|
|
960
1035
|
},
|
|
961
1036
|
mainMenu: {
|
|
962
1037
|
items: {
|
|
963
1038
|
practiceLabel: "\u7EC3\u4E60",
|
|
964
|
-
practiceHintWith: (
|
|
1039
|
+
practiceHintWith: (name) => `\u5F00\u59CB ${name}`,
|
|
965
1040
|
practiceHintNone: "\u8BF7\u5148\u9009\u8BCD\u5178",
|
|
966
1041
|
dictLabel: "\u8BCD\u5178",
|
|
967
1042
|
dictHint: "\u6D4F\u89C8\u3001\u4E0B\u8F7D\u3001\u8BBE\u4E3A\u9ED8\u8BA4",
|
|
@@ -971,18 +1046,19 @@ var zh = {
|
|
|
971
1046
|
statsHint: "\u5386\u53F2\u4E0E\u8D8B\u52BF",
|
|
972
1047
|
configLabel: "\u8BBE\u7F6E",
|
|
973
1048
|
configHint: "\u4FEE\u6539\u504F\u597D",
|
|
1049
|
+
stealthLabel: "\u6478\u9C7C",
|
|
1050
|
+
stealthHint: "\u5B89\u9759\u7EC3\u4E60\u6A21\u5F0F",
|
|
974
1051
|
quitLabel: "\u9000\u51FA",
|
|
975
|
-
quitHint: "Ctrl+C \
|
|
1052
|
+
quitHint: "Esc \u6216 Ctrl+C \u9000\u51FA"
|
|
976
1053
|
},
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE"
|
|
1054
|
+
hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE",
|
|
1055
|
+
helpHint: "? \u5E2E\u52A9"
|
|
980
1056
|
},
|
|
981
1057
|
dict: {
|
|
982
1058
|
title: "\u8BCD\u5178",
|
|
983
1059
|
loading: "\u52A0\u8F7D\u8BCD\u5178\u4E2D\u2026",
|
|
984
1060
|
entries: (n) => `${n} \u90E8\u8BCD\u5178`,
|
|
985
|
-
|
|
1061
|
+
filterPlaceholder: "\u8F93\u5165\u8FC7\u6EE4",
|
|
986
1062
|
local: "\u5DF2\u4E0B\u8F7D \u2713",
|
|
987
1063
|
notLocal: "\u672A\u4E0B\u8F7D",
|
|
988
1064
|
defaultMark: "\u9ED8\u8BA4 \u2605",
|
|
@@ -991,7 +1067,19 @@ var zh = {
|
|
|
991
1067
|
pulling: (id) => `\u62C9\u53D6 ${id} \u4E2D\u2026`,
|
|
992
1068
|
removing: (id) => `\u5220\u9664 ${id} \u4E2D\u2026`,
|
|
993
1069
|
errorOn: (id, msg) => `${id} \u51FA\u9519:${msg}`,
|
|
994
|
-
footer: "\u2191/\u2193 \u9009\u62E9 \xB7 Enter \
|
|
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
|
+
}
|
|
995
1083
|
},
|
|
996
1084
|
config: {
|
|
997
1085
|
title: "\u8BBE\u7F6E",
|
|
@@ -999,14 +1087,18 @@ var zh = {
|
|
|
999
1087
|
defaultDict: "\u9ED8\u8BA4\u8BCD\u5178",
|
|
1000
1088
|
defaultMode: "\u9ED8\u8BA4\u6A21\u5F0F",
|
|
1001
1089
|
accent: "\u53D1\u97F3",
|
|
1002
|
-
mirror: "\u955C\u50CF\u6E90",
|
|
1090
|
+
mirror: "\u8BCD\u5178\u955C\u50CF\u6E90",
|
|
1003
1091
|
chapterSize: "\u7AE0\u8282\u5355\u8BCD\u6570",
|
|
1004
1092
|
autoplayPronunciation: "\u81EA\u52A8\u64AD\u653E\u53D1\u97F3",
|
|
1005
1093
|
soundsMaster: "\u97F3\u6548\u603B\u5F00\u5173",
|
|
1006
1094
|
soundsKeystroke: "\u6309\u952E\u97F3",
|
|
1007
1095
|
soundsFeedback: "\u53CD\u9988\u97F3",
|
|
1008
1096
|
soundsKeySound: "\u6309\u952E\u97F3\u8272",
|
|
1009
|
-
language: "\u8BED\u8A00"
|
|
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" }
|
|
1010
1102
|
},
|
|
1011
1103
|
hints: {
|
|
1012
1104
|
editing: "\u8F93\u5165\u4FEE\u6539 \xB7 Enter \u4FDD\u5B58 \xB7 Esc \u53D6\u6D88",
|
|
@@ -1017,7 +1109,7 @@ var zh = {
|
|
|
1017
1109
|
}
|
|
1018
1110
|
},
|
|
1019
1111
|
stats: {
|
|
1020
|
-
title: "\u7EDF\u8BA1",
|
|
1112
|
+
title: "\u7EDF\u8BA1 \xB7 \u6982\u89C8",
|
|
1021
1113
|
loading: "\u52A0\u8F7D\u7EDF\u8BA1\u4E2D\u2026",
|
|
1022
1114
|
none: "\u8FD8\u6CA1\u6709\u7EC3\u4E60\u8BB0\u5F55\u3002",
|
|
1023
1115
|
nonePractice: "\u5148\u6765\u4E00\u6B21\u7EC3\u4E60\u5427\u3002",
|
|
@@ -1028,11 +1120,14 @@ var zh = {
|
|
|
1028
1120
|
wpm: "\u901F\u5EA6",
|
|
1029
1121
|
accuracy: "\u51C6\u786E\u7387",
|
|
1030
1122
|
streak: "\u8FDE\u7EED\u5929\u6570",
|
|
1031
|
-
last: (n) => `\u6700\u8FD1 ${n} \u5929 (
|
|
1032
|
-
cycleWindow: "
|
|
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",
|
|
1033
1125
|
recent: "\u6700\u8FD1\u4F1A\u8BDD",
|
|
1034
1126
|
topMistakes: "\u9AD8\u9891\u9519\u8BCD",
|
|
1035
|
-
footer: "
|
|
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`
|
|
1036
1131
|
},
|
|
1037
1132
|
word: {
|
|
1038
1133
|
title: "\u67E5\u8BCD",
|
|
@@ -1041,7 +1136,7 @@ var zh = {
|
|
|
1041
1136
|
pullFirst: "\u5148\u5728\u300C\u8BCD\u5178\u300D\u4E2D\u62C9\u53D6\u4E00\u90E8\u3002",
|
|
1042
1137
|
countAcross: (n) => `\u672C\u5730\u8BCD\u5178\u5171 ${n} \u8BCD`,
|
|
1043
1138
|
noMatches: (q) => `\u6CA1\u6709\u5339\u914D\u300C${q}\u300D\u7684\u8BCD`,
|
|
1044
|
-
inDict: (
|
|
1139
|
+
inDict: (name) => `\u6765\u6E90:${name}`,
|
|
1045
1140
|
mistakes: (n, date) => `\u9519\u8FC7 ${n} \u6B21 (\u6700\u8FD1 ${date})`,
|
|
1046
1141
|
footer: "\u8F93\u5165\u8FC7\u6EE4 \xB7 \u2191/\u2193 \u9009\u62E9 \xB7 Esc \u8FD4\u56DE"
|
|
1047
1142
|
},
|
|
@@ -1073,10 +1168,20 @@ var zh = {
|
|
|
1073
1168
|
accuracy: "\u51C6\u786E\u7387",
|
|
1074
1169
|
elapsed: (t) => `\u8017\u65F6 ${t}`
|
|
1075
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
|
+
},
|
|
1076
1183
|
footers: {
|
|
1077
|
-
typing: "Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD
|
|
1078
|
-
paused: "[r] \u7EE7\u7EED \xB7 [q] \u8FD4\u56DE\u83DC\u5355",
|
|
1079
|
-
summary: "[n] \u4E0B\u4E00\u7AE0 \xB7 [m] \u590D\u4E60\u9519\u8BCD \xB7 [q] \u8FD4\u56DE\u83DC\u5355"
|
|
1184
|
+
typing: "Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD"
|
|
1080
1185
|
},
|
|
1081
1186
|
errors: {
|
|
1082
1187
|
noMistakes: "\u9519\u8BCD\u672C\u662F\u7A7A\u7684\u3002\u5148\u7EC3\u4E60\u51E0\u7AE0\u5427\u3002",
|
|
@@ -1096,7 +1201,49 @@ var zh = {
|
|
|
1096
1201
|
accuracy: "\u51C6\u786E\u7387",
|
|
1097
1202
|
wpm: "\u901F\u5EA6",
|
|
1098
1203
|
newMistakes: "\u65B0\u9519\u8BCD",
|
|
1099
|
-
farewell: "\u4E0B\u6B21\u89C1\u3002"
|
|
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}%`
|
|
1100
1247
|
}
|
|
1101
1248
|
};
|
|
1102
1249
|
|
|
@@ -1145,13 +1292,55 @@ function pickStrings(pref) {
|
|
|
1145
1292
|
return { lang, t: lang === "zh" ? zh : en };
|
|
1146
1293
|
}
|
|
1147
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
|
+
|
|
1148
1337
|
// src/ui/screens/MainMenu.tsx
|
|
1149
|
-
import { useState as
|
|
1338
|
+
import { useState as useState6 } from "react";
|
|
1150
1339
|
import { Box as Box3, Text as Text2, useApp, useInput } from "ink";
|
|
1151
1340
|
|
|
1152
1341
|
// src/ui/components/BigWord.tsx
|
|
1153
1342
|
import { Box as Box2, Text, useStdout as useStdout2 } from "ink";
|
|
1154
|
-
import { jsx as
|
|
1343
|
+
import { jsx as jsx7, jsxs } from "react/jsx-runtime";
|
|
1155
1344
|
var PALETTE = {
|
|
1156
1345
|
accent: "#5eead4",
|
|
1157
1346
|
muted: "#6b7280",
|
|
@@ -1166,23 +1355,27 @@ function BigWord({ target, typed, error = false, hideTarget = false }) {
|
|
|
1166
1355
|
const cols = stdout?.columns ?? 80;
|
|
1167
1356
|
const chars = [...target];
|
|
1168
1357
|
const typedChars = [...typed];
|
|
1169
|
-
const sep = cols
|
|
1170
|
-
return /* @__PURE__ */
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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) => {
|
|
1361
|
+
const isTyped = i < typedChars.length;
|
|
1362
|
+
const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
|
|
1363
|
+
const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;
|
|
1364
|
+
return /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
|
|
1365
|
+
display,
|
|
1366
|
+
i < chars.length - 1 ? sep : ""
|
|
1367
|
+
] }, i);
|
|
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
|
+
] });
|
|
1186
1379
|
}
|
|
1187
1380
|
|
|
1188
1381
|
// src/util/text.ts
|
|
@@ -1200,45 +1393,85 @@ function visibleWidth2(s) {
|
|
|
1200
1393
|
return w;
|
|
1201
1394
|
}
|
|
1202
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";
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1203
1411
|
// src/ui/screens/MainMenu.tsx
|
|
1204
|
-
import { jsx as
|
|
1412
|
+
import { jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1205
1413
|
function MainMenu({ cfg }) {
|
|
1206
|
-
const [selected, setSelected] =
|
|
1414
|
+
const [selected, setSelected] = useState6(0);
|
|
1207
1415
|
const { exit } = useApp();
|
|
1208
1416
|
const nav = useNav();
|
|
1209
1417
|
const audio = useAudioStatus();
|
|
1210
1418
|
const t = useStrings();
|
|
1419
|
+
const defaultDictName = useDictName(cfg.defaultDict);
|
|
1211
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
|
+
};
|
|
1212
1436
|
const items = [
|
|
1213
1437
|
{
|
|
1214
1438
|
key: "p",
|
|
1215
1439
|
label: m.practiceLabel,
|
|
1216
|
-
hint: cfg.defaultDict ? m.practiceHintWith(
|
|
1217
|
-
run: () =>
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1440
|
+
hint: cfg.defaultDict ? m.practiceHintWith(truncateName(defaultDictName, 24)) : m.practiceHintNone,
|
|
1441
|
+
run: () => startPractice(cfg.stealth === "default")
|
|
1442
|
+
}
|
|
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(
|
|
1228
1453
|
{ key: "d", label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: "dict" }) },
|
|
1229
1454
|
{ key: "w", label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: "word" }) },
|
|
1230
1455
|
{ key: "s", label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: "stats" }) },
|
|
1231
1456
|
{ key: "c", label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: "config" }) },
|
|
1232
1457
|
{ key: "q", label: m.quitLabel, hint: m.quitHint, run: () => exit() }
|
|
1233
|
-
|
|
1458
|
+
);
|
|
1234
1459
|
const labelW = Math.max(...items.map((it) => visibleWidth2(it.label))) + 4;
|
|
1235
1460
|
useInput((input, key) => {
|
|
1461
|
+
if (key.escape) {
|
|
1462
|
+
exit();
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1236
1465
|
if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);
|
|
1237
1466
|
if (key.downArrow) setSelected((i) => (i + 1) % items.length);
|
|
1238
1467
|
if (key.return) {
|
|
1239
1468
|
items[selected].run();
|
|
1240
1469
|
return;
|
|
1241
1470
|
}
|
|
1471
|
+
if (input === "?") {
|
|
1472
|
+
nav.navigate({ name: "help" });
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1242
1475
|
for (const it of items) {
|
|
1243
1476
|
if (input === it.key) {
|
|
1244
1477
|
it.run();
|
|
@@ -1248,39 +1481,42 @@ function MainMenu({ cfg }) {
|
|
|
1248
1481
|
});
|
|
1249
1482
|
return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", children: [
|
|
1250
1483
|
/* @__PURE__ */ jsxs2(Box3, { children: [
|
|
1251
|
-
/* @__PURE__ */
|
|
1484
|
+
/* @__PURE__ */ jsx8(Text2, { bold: true, color: PALETTE.accent, children: t.app.title }),
|
|
1252
1485
|
/* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
|
|
1253
1486
|
" \xB7 ",
|
|
1254
1487
|
t.app.subtitle
|
|
1255
1488
|
] })
|
|
1256
1489
|
] }),
|
|
1257
|
-
/* @__PURE__ */
|
|
1490
|
+
/* @__PURE__ */ jsx8(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
|
|
1258
1491
|
const active = i === selected;
|
|
1259
1492
|
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(it.label)));
|
|
1260
1493
|
return /* @__PURE__ */ jsxs2(Box3, { children: [
|
|
1261
|
-
/* @__PURE__ */
|
|
1494
|
+
/* @__PURE__ */ jsx8(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
1262
1495
|
/* @__PURE__ */ jsxs2(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
|
|
1263
1496
|
"[",
|
|
1264
1497
|
it.key,
|
|
1265
1498
|
"]"
|
|
1266
1499
|
] }),
|
|
1267
|
-
/* @__PURE__ */
|
|
1500
|
+
/* @__PURE__ */ jsx8(Text2, { children: " " }),
|
|
1268
1501
|
/* @__PURE__ */ jsxs2(Text2, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
|
|
1269
1502
|
it.label,
|
|
1270
1503
|
pad
|
|
1271
1504
|
] }),
|
|
1272
|
-
/* @__PURE__ */
|
|
1505
|
+
/* @__PURE__ */ jsx8(Text2, { color: PALETTE.muted, children: it.hint })
|
|
1273
1506
|
] }, it.key);
|
|
1274
1507
|
}) }),
|
|
1275
|
-
/* @__PURE__ */
|
|
1276
|
-
|
|
1277
|
-
|
|
1508
|
+
/* @__PURE__ */ jsx8(Box3, { marginTop: 2, children: /* @__PURE__ */ jsxs2(Text2, { color: PALETTE.muted, children: [
|
|
1509
|
+
t.mainMenu.hint,
|
|
1510
|
+
" \xB7 ",
|
|
1511
|
+
t.mainMenu.helpHint
|
|
1512
|
+
] }) }),
|
|
1513
|
+
audio.warning && /* @__PURE__ */ jsx8(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text2, { color: PALETTE.warning, children: t.audio.noPlayer }) })
|
|
1278
1514
|
] });
|
|
1279
1515
|
}
|
|
1280
1516
|
|
|
1281
1517
|
// src/ui/screens/PracticeScreen.tsx
|
|
1282
|
-
import { useState as
|
|
1283
|
-
import { Box as
|
|
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";
|
|
1284
1520
|
|
|
1285
1521
|
// src/util/shuffle.ts
|
|
1286
1522
|
function shuffle(arr, rng = Math.random) {
|
|
@@ -1479,7 +1715,7 @@ function topN(book, n) {
|
|
|
1479
1715
|
}
|
|
1480
1716
|
|
|
1481
1717
|
// src/ui/hooks/useWordLoop.ts
|
|
1482
|
-
import { useEffect as
|
|
1718
|
+
import { useEffect as useEffect4, useReducer, useRef, useState as useState7 } from "react";
|
|
1483
1719
|
import { useInput as useInput2, useApp as useApp2 } from "ink";
|
|
1484
1720
|
function reducer(state2, action) {
|
|
1485
1721
|
if (action.type === "start") {
|
|
@@ -1513,7 +1749,7 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1513
1749
|
lastEffect: null
|
|
1514
1750
|
}));
|
|
1515
1751
|
const completedRef = useRef(false);
|
|
1516
|
-
const [tick, setTick] =
|
|
1752
|
+
const [tick, setTick] = useState7(0);
|
|
1517
1753
|
const { exit } = useApp2();
|
|
1518
1754
|
useInput2(
|
|
1519
1755
|
(input, key) => {
|
|
@@ -1545,13 +1781,13 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1545
1781
|
},
|
|
1546
1782
|
{ isActive: enabled }
|
|
1547
1783
|
);
|
|
1548
|
-
|
|
1784
|
+
useEffect4(() => {
|
|
1549
1785
|
if (state2.session.finishedAt !== null && !completedRef.current) {
|
|
1550
1786
|
completedRef.current = true;
|
|
1551
1787
|
onComplete(state2.session);
|
|
1552
1788
|
}
|
|
1553
1789
|
}, [state2.session, onComplete]);
|
|
1554
|
-
|
|
1790
|
+
useEffect4(() => {
|
|
1555
1791
|
if (state2.session.finishedAt !== null) return;
|
|
1556
1792
|
const id = setInterval(() => setTick((t) => t + 1), 1e3);
|
|
1557
1793
|
return () => clearInterval(id);
|
|
@@ -1560,10 +1796,10 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1560
1796
|
}
|
|
1561
1797
|
|
|
1562
1798
|
// src/ui/hooks/useAudio.ts
|
|
1563
|
-
import { useEffect as
|
|
1799
|
+
import { useEffect as useEffect5, useRef as useRef2 } from "react";
|
|
1564
1800
|
function useAudio(opts) {
|
|
1565
1801
|
const initedRef = useRef2(false);
|
|
1566
|
-
|
|
1802
|
+
useEffect5(() => {
|
|
1567
1803
|
if (initedRef.current) return;
|
|
1568
1804
|
initedRef.current = true;
|
|
1569
1805
|
initAudio(!opts.enabled).catch(() => void 0);
|
|
@@ -1737,16 +1973,80 @@ function useSessionPersistence(meta) {
|
|
|
1737
1973
|
);
|
|
1738
1974
|
}
|
|
1739
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
|
+
|
|
1740
2040
|
// src/ui/screens/PracticeScreen.tsx
|
|
1741
|
-
import { jsx as
|
|
2041
|
+
import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1742
2042
|
function PracticeScreen({ params }) {
|
|
1743
2043
|
const { dictId, chapterIndex, mode } = params;
|
|
1744
2044
|
const { cfg } = useAppState();
|
|
1745
2045
|
const t = useStrings();
|
|
1746
|
-
const [phase, setPhase] =
|
|
1747
|
-
const [loaded, setLoaded] =
|
|
1748
|
-
const [errorMsg, setErrorMsg] =
|
|
1749
|
-
|
|
2046
|
+
const [phase, setPhase] = useState8("loading");
|
|
2047
|
+
const [loaded, setLoaded] = useState8(null);
|
|
2048
|
+
const [errorMsg, setErrorMsg] = useState8(null);
|
|
2049
|
+
useEffect6(() => {
|
|
1750
2050
|
let cancelled = false;
|
|
1751
2051
|
setPhase("loading");
|
|
1752
2052
|
setLoaded(null);
|
|
@@ -1789,13 +2089,13 @@ function PracticeScreen({ params }) {
|
|
|
1789
2089
|
};
|
|
1790
2090
|
}, [dictId, chapterIndex, mode, cfg.chapterSize, t]);
|
|
1791
2091
|
if (phase === "loading") {
|
|
1792
|
-
return /* @__PURE__ */
|
|
2092
|
+
return /* @__PURE__ */ jsx10(Centered, { text: t.practice.loading, color: PALETTE.muted });
|
|
1793
2093
|
}
|
|
1794
2094
|
if (phase === "error") {
|
|
1795
|
-
return /* @__PURE__ */
|
|
2095
|
+
return /* @__PURE__ */ jsx10(ErrorView, { msg: errorMsg ?? t.practice.errors.unknown });
|
|
1796
2096
|
}
|
|
1797
2097
|
if (!loaded) return null;
|
|
1798
|
-
return /* @__PURE__ */
|
|
2098
|
+
return /* @__PURE__ */ jsx10(
|
|
1799
2099
|
PracticeRunner,
|
|
1800
2100
|
{
|
|
1801
2101
|
params,
|
|
@@ -1803,7 +2103,7 @@ function PracticeScreen({ params }) {
|
|
|
1803
2103
|
phase,
|
|
1804
2104
|
setPhase
|
|
1805
2105
|
},
|
|
1806
|
-
`${dictId}-${chapterIndex}-${mode}`
|
|
2106
|
+
`${dictId}-${chapterIndex}-${mode}-${params.stealth ? "s" : "n"}`
|
|
1807
2107
|
);
|
|
1808
2108
|
}
|
|
1809
2109
|
function PracticeRunner({
|
|
@@ -1813,19 +2113,22 @@ function PracticeRunner({
|
|
|
1813
2113
|
setPhase
|
|
1814
2114
|
}) {
|
|
1815
2115
|
const { dictId, chapterIndex, mode } = params;
|
|
2116
|
+
const stealth = params.stealth === true;
|
|
1816
2117
|
const { cfg } = useAppState();
|
|
1817
2118
|
const nav = useNav();
|
|
1818
2119
|
const { exit } = useApp3();
|
|
1819
2120
|
const goBack = () => nav.stack.length > 1 ? nav.back() : exit();
|
|
1820
2121
|
const persist = useSessionPersistence({ dictId, chapterIndex, mode });
|
|
2122
|
+
const dictName = useDictName(dictId);
|
|
1821
2123
|
const audio = useAudio({
|
|
1822
|
-
enabled: cfg.sounds.master,
|
|
2124
|
+
enabled: !stealth && cfg.sounds.master,
|
|
1823
2125
|
accent: cfg.accent,
|
|
1824
|
-
autoplayPronunciation: cfg.autoplayPronunciation
|
|
2126
|
+
autoplayPronunciation: !stealth && cfg.autoplayPronunciation
|
|
1825
2127
|
});
|
|
1826
2128
|
const finishedRef = useRef3(false);
|
|
1827
2129
|
const lastEffectRef = useRef3(null);
|
|
1828
2130
|
const lastIndexRef = useRef3(-1);
|
|
2131
|
+
const [infoVisible, setInfoVisible] = useState8(false);
|
|
1829
2132
|
const { session, lastEffect, tick } = useWordLoop({
|
|
1830
2133
|
playlist: loaded.playlist,
|
|
1831
2134
|
enabled: phase === "typing",
|
|
@@ -1838,12 +2141,13 @@ function PracticeRunner({
|
|
|
1838
2141
|
});
|
|
1839
2142
|
},
|
|
1840
2143
|
onEscape: () => setPhase(phase === "paused" ? "typing" : "paused"),
|
|
1841
|
-
onTab: () => {
|
|
2144
|
+
onTab: stealth ? void 0 : () => {
|
|
1842
2145
|
const cur = session.current ? loaded.playlist[session.current.wordIndex] : void 0;
|
|
1843
2146
|
if (cur) void audio.pronounce(cur.name);
|
|
1844
2147
|
}
|
|
1845
2148
|
});
|
|
1846
|
-
|
|
2149
|
+
useEffect6(() => {
|
|
2150
|
+
if (stealth) return;
|
|
1847
2151
|
if (lastEffect === null) return;
|
|
1848
2152
|
if (lastEffect === lastEffectRef.current) return;
|
|
1849
2153
|
lastEffectRef.current = lastEffect;
|
|
@@ -1853,8 +2157,9 @@ function PracticeRunner({
|
|
|
1853
2157
|
if (cfg.sounds.feedback) audio.correct();
|
|
1854
2158
|
if (cfg.sounds.keystroke) audio.keystroke();
|
|
1855
2159
|
}
|
|
1856
|
-
}, [lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
|
|
1857
|
-
|
|
2160
|
+
}, [stealth, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
|
|
2161
|
+
useEffect6(() => {
|
|
2162
|
+
if (stealth) return;
|
|
1858
2163
|
const idx = session.current?.wordIndex ?? -1;
|
|
1859
2164
|
if (idx === -1) return;
|
|
1860
2165
|
if (idx === lastIndexRef.current) return;
|
|
@@ -1863,63 +2168,148 @@ function PracticeRunner({
|
|
|
1863
2168
|
const next = loaded.playlist[idx + 1];
|
|
1864
2169
|
if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);
|
|
1865
2170
|
if (next) audio.prefetch(next.name);
|
|
1866
|
-
}, [session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
|
|
2171
|
+
}, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
|
|
1867
2172
|
void tick;
|
|
1868
2173
|
useInput3(
|
|
1869
|
-
(input) => {
|
|
1870
|
-
if (input === "
|
|
1871
|
-
|
|
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
|
+
}
|
|
1872
2192
|
},
|
|
1873
2193
|
{ isActive: phase === "paused" }
|
|
1874
2194
|
);
|
|
1875
2195
|
useInput3(
|
|
1876
|
-
(input) => {
|
|
1877
|
-
if (
|
|
2196
|
+
(input, key) => {
|
|
2197
|
+
if (key.escape) {
|
|
1878
2198
|
goBack();
|
|
1879
2199
|
return;
|
|
1880
2200
|
}
|
|
1881
|
-
if (
|
|
2201
|
+
if (key.return) {
|
|
1882
2202
|
const nextIdx = chapterIndex + 1;
|
|
1883
2203
|
if (mode === "loop") {
|
|
1884
|
-
nav.replace({
|
|
2204
|
+
nav.replace({
|
|
2205
|
+
name: "practice",
|
|
2206
|
+
params: { dictId, chapterIndex, mode, stealth: params.stealth }
|
|
2207
|
+
});
|
|
1885
2208
|
} else if (mode === "review" || nextIdx >= loaded.totalChapters) {
|
|
1886
2209
|
goBack();
|
|
1887
2210
|
} else {
|
|
1888
|
-
nav.replace({
|
|
2211
|
+
nav.replace({
|
|
2212
|
+
name: "practice",
|
|
2213
|
+
params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth }
|
|
2214
|
+
});
|
|
1889
2215
|
}
|
|
1890
2216
|
return;
|
|
1891
2217
|
}
|
|
1892
2218
|
if (input === "m") {
|
|
1893
|
-
nav.replace({
|
|
2219
|
+
nav.replace({
|
|
2220
|
+
name: "practice",
|
|
2221
|
+
params: { dictId, chapterIndex: 0, mode: "review", stealth: params.stealth }
|
|
2222
|
+
});
|
|
1894
2223
|
return;
|
|
1895
2224
|
}
|
|
1896
2225
|
},
|
|
1897
2226
|
{ isActive: phase === "summary" }
|
|
1898
2227
|
);
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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(
|
|
1902
2297
|
SummaryView,
|
|
1903
2298
|
{
|
|
1904
|
-
|
|
2299
|
+
dictName,
|
|
1905
2300
|
chapterIndex,
|
|
1906
2301
|
totalChapters: loaded.totalChapters,
|
|
1907
2302
|
mode,
|
|
1908
|
-
summary
|
|
2303
|
+
summary
|
|
1909
2304
|
}
|
|
1910
2305
|
);
|
|
1911
2306
|
}
|
|
1912
2307
|
const currentWord = session.current ? loaded.playlist[session.current.wordIndex] : loaded.playlist[loaded.playlist.length - 1];
|
|
1913
2308
|
const inputState = session.current?.input ?? { target: "", typed: "", errorsThisWord: 0 };
|
|
1914
|
-
|
|
1915
|
-
const completed = session.results.length;
|
|
1916
|
-
const errors = session.results.reduce((a, r) => a + r.errors, 0);
|
|
1917
|
-
const minutes = elapsedMs / 6e4;
|
|
1918
|
-
const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
|
|
1919
|
-
return /* @__PURE__ */ jsx8(
|
|
2309
|
+
return /* @__PURE__ */ jsx10(
|
|
1920
2310
|
TypingLayout,
|
|
1921
2311
|
{
|
|
1922
|
-
|
|
2312
|
+
dictName,
|
|
1923
2313
|
chapterIndex,
|
|
1924
2314
|
totalChapters: loaded.totalChapters,
|
|
1925
2315
|
mode,
|
|
@@ -1943,7 +2333,7 @@ function pickPhonetic(word, accent) {
|
|
|
1943
2333
|
const p = accent === "us" ? word.usphone : word.ukphone;
|
|
1944
2334
|
return p ? `/${p}/` : null;
|
|
1945
2335
|
}
|
|
1946
|
-
function
|
|
2336
|
+
function fmtTime2(ms) {
|
|
1947
2337
|
const total = Math.floor(ms / 1e3);
|
|
1948
2338
|
const m = Math.floor(total / 60);
|
|
1949
2339
|
const s = total % 60;
|
|
@@ -1952,11 +2342,11 @@ function fmtTime(ms) {
|
|
|
1952
2342
|
function TypingLayout(props) {
|
|
1953
2343
|
const t = useStrings();
|
|
1954
2344
|
const progressFrac = props.total === 0 ? 0 : props.completed / props.total;
|
|
1955
|
-
return /* @__PURE__ */
|
|
1956
|
-
/* @__PURE__ */
|
|
2345
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2346
|
+
/* @__PURE__ */ jsx10(
|
|
1957
2347
|
StatusBar,
|
|
1958
2348
|
{
|
|
1959
|
-
|
|
2349
|
+
dictName: props.dictName,
|
|
1960
2350
|
chapterIndex: props.chapterIndex,
|
|
1961
2351
|
totalChapters: props.totalChapters,
|
|
1962
2352
|
mode: props.mode,
|
|
@@ -1966,8 +2356,8 @@ function TypingLayout(props) {
|
|
|
1966
2356
|
elapsedMs: props.elapsedMs
|
|
1967
2357
|
}
|
|
1968
2358
|
),
|
|
1969
|
-
/* @__PURE__ */
|
|
1970
|
-
/* @__PURE__ */
|
|
2359
|
+
/* @__PURE__ */ jsxs4(Box5, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
|
|
2360
|
+
/* @__PURE__ */ jsx10(
|
|
1971
2361
|
BigWord,
|
|
1972
2362
|
{
|
|
1973
2363
|
target: props.target,
|
|
@@ -1976,17 +2366,17 @@ function TypingLayout(props) {
|
|
|
1976
2366
|
hideTarget: props.hideTarget
|
|
1977
2367
|
}
|
|
1978
2368
|
),
|
|
1979
|
-
props.phonetic && /* @__PURE__ */
|
|
1980
|
-
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)) })
|
|
1981
2371
|
] }),
|
|
1982
|
-
/* @__PURE__ */
|
|
1983
|
-
/* @__PURE__ */
|
|
1984
|
-
/* @__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: [
|
|
1985
2375
|
props.completed,
|
|
1986
2376
|
"/",
|
|
1987
2377
|
props.total,
|
|
1988
2378
|
" \xB7 ",
|
|
1989
|
-
|
|
2379
|
+
fmtTime2(props.elapsedMs),
|
|
1990
2380
|
" \xB7 ",
|
|
1991
2381
|
props.wpm,
|
|
1992
2382
|
" ",
|
|
@@ -1996,7 +2386,7 @@ function TypingLayout(props) {
|
|
|
1996
2386
|
" ",
|
|
1997
2387
|
t.practice.statCards.errors
|
|
1998
2388
|
] }) }),
|
|
1999
|
-
/* @__PURE__ */
|
|
2389
|
+
/* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.footers.typing }) })
|
|
2000
2390
|
] })
|
|
2001
2391
|
] });
|
|
2002
2392
|
}
|
|
@@ -2004,12 +2394,13 @@ function StatusBar(props) {
|
|
|
2004
2394
|
const t = useStrings();
|
|
2005
2395
|
const modeName = t.practice.modes[props.mode];
|
|
2006
2396
|
const accentName = t.practice.accents[props.accent];
|
|
2007
|
-
const
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
/* @__PURE__ */
|
|
2012
|
-
/* @__PURE__ */
|
|
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 })
|
|
2013
2404
|
] });
|
|
2014
2405
|
}
|
|
2015
2406
|
function ProgressBar({ frac }) {
|
|
@@ -2017,38 +2408,43 @@ function ProgressBar({ frac }) {
|
|
|
2017
2408
|
const width = Math.max(20, Math.min(72, cols - 16));
|
|
2018
2409
|
const filled = Math.round(width * Math.max(0, Math.min(1, frac)));
|
|
2019
2410
|
const empty = width - filled;
|
|
2020
|
-
return /* @__PURE__ */
|
|
2021
|
-
/* @__PURE__ */
|
|
2022
|
-
/* @__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) })
|
|
2023
2414
|
] });
|
|
2024
2415
|
}
|
|
2025
|
-
function PausedView() {
|
|
2416
|
+
function PausedView(props) {
|
|
2026
2417
|
const t = useStrings();
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
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 }) })
|
|
2030
2426
|
] });
|
|
2031
2427
|
}
|
|
2032
2428
|
function ErrorView({ msg }) {
|
|
2033
2429
|
const t = useStrings();
|
|
2034
|
-
return /* @__PURE__ */
|
|
2035
|
-
/* @__PURE__ */
|
|
2036
|
-
/* @__PURE__ */
|
|
2037
|
-
"
|
|
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 ",
|
|
2038
2434
|
t.common.back
|
|
2039
2435
|
] }) }),
|
|
2040
|
-
/* @__PURE__ */
|
|
2436
|
+
/* @__PURE__ */ jsx10(BackKey, {})
|
|
2041
2437
|
] });
|
|
2042
2438
|
}
|
|
2043
2439
|
function BackKey() {
|
|
2044
2440
|
const nav = useNav();
|
|
2045
|
-
useInput3((
|
|
2046
|
-
if (
|
|
2441
|
+
useInput3((_input, key) => {
|
|
2442
|
+
if (key.escape) nav.back();
|
|
2047
2443
|
});
|
|
2048
2444
|
return null;
|
|
2049
2445
|
}
|
|
2050
2446
|
function Centered({ text, color }) {
|
|
2051
|
-
return /* @__PURE__ */
|
|
2447
|
+
return /* @__PURE__ */ jsx10(Box5, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx10(Text4, { color, children: text }) });
|
|
2052
2448
|
}
|
|
2053
2449
|
function SummaryView(props) {
|
|
2054
2450
|
const { summary } = props;
|
|
@@ -2059,13 +2455,16 @@ function SummaryView(props) {
|
|
|
2059
2455
|
const accPct = Math.round(acc * 1e3) / 10;
|
|
2060
2456
|
const t = useStrings();
|
|
2061
2457
|
const modeName = t.practice.modes[props.mode];
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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(
|
|
2069
2468
|
StatCard,
|
|
2070
2469
|
{
|
|
2071
2470
|
label: t.practice.statCards.errors,
|
|
@@ -2073,36 +2472,107 @@ function SummaryView(props) {
|
|
|
2073
2472
|
color: summary.errors > 0 ? PALETTE.error : PALETTE.muted
|
|
2074
2473
|
}
|
|
2075
2474
|
),
|
|
2076
|
-
/* @__PURE__ */
|
|
2077
|
-
/* @__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 })
|
|
2078
2477
|
] }),
|
|
2079
|
-
/* @__PURE__ */
|
|
2080
|
-
/* @__PURE__ */
|
|
2081
|
-
/* @__PURE__ */
|
|
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 }) })
|
|
2082
2481
|
] });
|
|
2083
2482
|
}
|
|
2084
2483
|
function StatCard({ label, value, color }) {
|
|
2085
|
-
return /* @__PURE__ */
|
|
2086
|
-
/* @__PURE__ */
|
|
2087
|
-
/* @__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 })
|
|
2088
2487
|
] });
|
|
2089
2488
|
}
|
|
2090
2489
|
|
|
2091
2490
|
// src/ui/screens/DictBrowser.tsx
|
|
2092
|
-
import { useEffect as
|
|
2093
|
-
import { Box as
|
|
2094
|
-
|
|
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";
|
|
2095
2564
|
function DictBrowser({ params }) {
|
|
2096
2565
|
const nav = useNav();
|
|
2097
2566
|
const { cfg, setCfg } = useAppState();
|
|
2098
2567
|
const t = useStrings();
|
|
2099
|
-
const
|
|
2100
|
-
const [
|
|
2101
|
-
const [
|
|
2102
|
-
const [
|
|
2103
|
-
const [
|
|
2104
|
-
const [pending, setPending] =
|
|
2105
|
-
const [tick, setTick] =
|
|
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);
|
|
2106
2576
|
const refresh = async () => {
|
|
2107
2577
|
const reg = await loadRegistry();
|
|
2108
2578
|
const flagged = await Promise.all(
|
|
@@ -2111,141 +2581,221 @@ function DictBrowser({ params }) {
|
|
|
2111
2581
|
setRows(flagged);
|
|
2112
2582
|
setLoading(false);
|
|
2113
2583
|
};
|
|
2114
|
-
|
|
2584
|
+
useEffect7(() => {
|
|
2115
2585
|
void refresh();
|
|
2116
2586
|
}, [tick]);
|
|
2117
|
-
const filtered =
|
|
2587
|
+
const filtered = useMemo2(
|
|
2588
|
+
() => filter ? rows.filter((r) => filterRegistry([r.entry], filter).length > 0) : rows,
|
|
2589
|
+
[filter, rows]
|
|
2590
|
+
);
|
|
2118
2591
|
const safeSelected = Math.max(0, Math.min(filtered.length - 1, selected));
|
|
2119
2592
|
const current = filtered[safeSelected];
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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"
|
|
2125
2606
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
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 });
|
|
2129
2630
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
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 });
|
|
2132
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();
|
|
2133
2656
|
return;
|
|
2134
2657
|
}
|
|
2135
|
-
if (key.upArrow)
|
|
2136
|
-
|
|
2137
|
-
if (input === "/") {
|
|
2138
|
-
setFilterFocus(true);
|
|
2658
|
+
if (key.upArrow) {
|
|
2659
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
2139
2660
|
return;
|
|
2140
2661
|
}
|
|
2141
|
-
if (key.
|
|
2142
|
-
|
|
2662
|
+
if (key.downArrow) {
|
|
2663
|
+
setSelected((i) => Math.min(filtered.length - 1, i + 1));
|
|
2143
2664
|
return;
|
|
2144
2665
|
}
|
|
2145
|
-
if (
|
|
2146
|
-
|
|
2147
|
-
void (async () => {
|
|
2148
|
-
await setCfg({ ...cfg, defaultDict: current.entry.id });
|
|
2149
|
-
if (params?.pickerMode === "choose-then-practice") {
|
|
2150
|
-
nav.replace({
|
|
2151
|
-
name: "practice",
|
|
2152
|
-
params: { dictId: current.entry.id, chapterIndex: 0, mode: cfg.defaultMode }
|
|
2153
|
-
});
|
|
2154
|
-
} else {
|
|
2155
|
-
nav.back();
|
|
2156
|
-
}
|
|
2157
|
-
})();
|
|
2666
|
+
if (key.ctrl && input === "k") {
|
|
2667
|
+
setPanel("more");
|
|
2158
2668
|
return;
|
|
2159
2669
|
}
|
|
2160
|
-
if (
|
|
2161
|
-
|
|
2162
|
-
name: "practice",
|
|
2163
|
-
params: { dictId: current.entry.id, chapterIndex: 0, mode: cfg.defaultMode }
|
|
2164
|
-
});
|
|
2670
|
+
if (key.return) {
|
|
2671
|
+
if (current) setPanel("item");
|
|
2165
2672
|
return;
|
|
2166
2673
|
}
|
|
2167
|
-
if (
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
try {
|
|
2171
|
-
await removeDictionary(current.entry.id);
|
|
2172
|
-
setPending(null);
|
|
2173
|
-
setTick((n) => n + 1);
|
|
2174
|
-
} catch (err) {
|
|
2175
|
-
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
2176
|
-
}
|
|
2177
|
-
})();
|
|
2674
|
+
if (key.backspace || key.delete) {
|
|
2675
|
+
setFilter((f) => f.slice(0, -1));
|
|
2676
|
+
setSelected(0);
|
|
2178
2677
|
return;
|
|
2179
2678
|
}
|
|
2180
|
-
if (input ===
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
try {
|
|
2184
|
-
await pullDictionary(current.entry.id);
|
|
2185
|
-
setPending(null);
|
|
2186
|
-
setTick((n) => n + 1);
|
|
2187
|
-
} catch (err) {
|
|
2188
|
-
setPending({ kind: "error", id: current.entry.id, msg: err.message });
|
|
2189
|
-
}
|
|
2190
|
-
})();
|
|
2679
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
2680
|
+
setFilter((f) => f + input);
|
|
2681
|
+
setSelected(0);
|
|
2191
2682
|
}
|
|
2192
2683
|
});
|
|
2193
2684
|
if (loading) {
|
|
2194
|
-
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 }) });
|
|
2195
2686
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
+
] })
|
|
2201
2746
|
] }),
|
|
2202
|
-
/* @__PURE__ */
|
|
2203
|
-
/* @__PURE__ */
|
|
2204
|
-
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;
|
|
2205
2750
|
const active = i === safeSelected;
|
|
2206
2751
|
const isDefault = cfg.defaultDict === row.entry.id;
|
|
2207
|
-
return /* @__PURE__ */
|
|
2208
|
-
/* @__PURE__ */
|
|
2209
|
-
/* @__PURE__ */
|
|
2210
|
-
/* @__PURE__ */
|
|
2211
|
-
/* @__PURE__ */
|
|
2212
|
-
/* @__PURE__ */
|
|
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) }) })
|
|
2213
2758
|
] }, row.entry.id);
|
|
2214
2759
|
}) }),
|
|
2215
|
-
/* @__PURE__ */
|
|
2216
|
-
/* @__PURE__ */
|
|
2217
|
-
/* @__PURE__ */
|
|
2218
|
-
/* @__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: [
|
|
2219
2764
|
current.entry.language,
|
|
2220
2765
|
" \xB7 ",
|
|
2221
2766
|
current.entry.category
|
|
2222
2767
|
] }) }),
|
|
2223
|
-
/* @__PURE__ */
|
|
2224
|
-
current.entry.description && /* @__PURE__ */
|
|
2225
|
-
current.entry.tags.length > 0 && /* @__PURE__ */
|
|
2226
|
-
/* @__PURE__ */
|
|
2227
|
-
cfg.defaultDict === current.entry.id && /* @__PURE__ */
|
|
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 }) })
|
|
2228
2773
|
] }) })
|
|
2229
2774
|
] }),
|
|
2230
|
-
pending && /* @__PURE__ */
|
|
2231
|
-
pending.kind === "pulling" && /* @__PURE__ */
|
|
2232
|
-
pending.kind === "removing" && /* @__PURE__ */
|
|
2233
|
-
pending.kind === "
|
|
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,
|
|
2780
|
+
"\u2026"
|
|
2781
|
+
] }),
|
|
2782
|
+
pending.kind === "error" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.error, children: t.dict.errorOn(pending.id, pending.msg) })
|
|
2234
2783
|
] }),
|
|
2235
|
-
/* @__PURE__ */
|
|
2784
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.footer }) })
|
|
2236
2785
|
] });
|
|
2237
2786
|
}
|
|
2238
2787
|
|
|
2239
2788
|
// src/ui/screens/ConfigEditor.tsx
|
|
2240
|
-
import { useState as
|
|
2241
|
-
import { Box as
|
|
2242
|
-
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";
|
|
2243
2792
|
var FIELDS = [
|
|
2244
2793
|
{ kind: "dictRef", path: "defaultDict", labelKey: "defaultDict" },
|
|
2245
2794
|
{ kind: "enum", path: "defaultMode", labelKey: "defaultMode", options: ["order", "dictation", "review", "random", "loop"] },
|
|
2246
2795
|
{ kind: "enum", path: "accent", labelKey: "accent", options: ["us", "uk"] },
|
|
2247
2796
|
{ kind: "enum", path: "language", labelKey: "language", options: ["auto", "zh", "en"] },
|
|
2248
2797
|
{ kind: "enum", path: "mirror", labelKey: "mirror", options: ["jsdelivr", "github"] },
|
|
2798
|
+
{ kind: "enum", path: "stealth", labelKey: "stealth", options: ["off", "menu", "default"] },
|
|
2249
2799
|
{ kind: "int", path: "chapterSize", labelKey: "chapterSize", min: 1, max: 200 },
|
|
2250
2800
|
{ kind: "bool", path: "autoplayPronunciation", labelKey: "autoplayPronunciation" },
|
|
2251
2801
|
{ kind: "bool", path: "sounds.master", labelKey: "soundsMaster" },
|
|
@@ -2263,10 +2813,11 @@ function ConfigEditor() {
|
|
|
2263
2813
|
const nav = useNav();
|
|
2264
2814
|
const { cfg, setCfg } = useAppState();
|
|
2265
2815
|
const t = useStrings();
|
|
2266
|
-
const
|
|
2267
|
-
const [
|
|
2268
|
-
const [
|
|
2269
|
-
const [
|
|
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);
|
|
2270
2821
|
const field = FIELDS[selected];
|
|
2271
2822
|
const currentValue = getByPath2(cfg, field.path);
|
|
2272
2823
|
const commit = async (raw) => {
|
|
@@ -2279,7 +2830,7 @@ function ConfigEditor() {
|
|
|
2279
2830
|
setError(err.message);
|
|
2280
2831
|
}
|
|
2281
2832
|
};
|
|
2282
|
-
|
|
2833
|
+
useInput6((input, key) => {
|
|
2283
2834
|
if (editing && field.kind === "string") {
|
|
2284
2835
|
if (key.escape) {
|
|
2285
2836
|
setEditing(false);
|
|
@@ -2314,7 +2865,7 @@ function ConfigEditor() {
|
|
|
2314
2865
|
if (/^[0-9]$/.test(input)) setDraft((d) => d + input);
|
|
2315
2866
|
return;
|
|
2316
2867
|
}
|
|
2317
|
-
if (key.escape
|
|
2868
|
+
if (key.escape) {
|
|
2318
2869
|
nav.back();
|
|
2319
2870
|
return;
|
|
2320
2871
|
}
|
|
@@ -2350,35 +2901,51 @@ function ConfigEditor() {
|
|
|
2350
2901
|
}
|
|
2351
2902
|
});
|
|
2352
2903
|
const labelW = Math.max(...FIELDS.map((f) => visibleWidth2(t.config.fields[f.labelKey]))) + 4;
|
|
2353
|
-
return /* @__PURE__ */
|
|
2354
|
-
/* @__PURE__ */
|
|
2355
|
-
/* @__PURE__ */
|
|
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) => {
|
|
2356
2907
|
const active = i === selected;
|
|
2357
2908
|
const value = getByPath2(cfg, f.path);
|
|
2358
|
-
const display = renderValue(
|
|
2909
|
+
const display = renderValue(
|
|
2910
|
+
f,
|
|
2911
|
+
value,
|
|
2912
|
+
active && editing ? draft : null,
|
|
2913
|
+
t,
|
|
2914
|
+
f.path === "defaultDict" ? defaultDictName : ""
|
|
2915
|
+
);
|
|
2359
2916
|
const label = t.config.fields[f.labelKey];
|
|
2360
2917
|
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
|
|
2361
|
-
return /* @__PURE__ */
|
|
2362
|
-
/* @__PURE__ */
|
|
2363
|
-
/* @__PURE__ */
|
|
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: [
|
|
2364
2921
|
label,
|
|
2365
2922
|
pad
|
|
2366
2923
|
] }),
|
|
2367
|
-
/* @__PURE__ */
|
|
2924
|
+
/* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: display })
|
|
2368
2925
|
] }, f.path);
|
|
2369
2926
|
}) }),
|
|
2370
|
-
error && /* @__PURE__ */
|
|
2927
|
+
error && /* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: PALETTE.error, children: [
|
|
2371
2928
|
"! ",
|
|
2372
2929
|
error
|
|
2373
2930
|
] }) }),
|
|
2374
|
-
/* @__PURE__ */
|
|
2931
|
+
/* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text7, { color: PALETTE.muted, children: hintFor(field, editing, t) }) })
|
|
2375
2932
|
] });
|
|
2376
2933
|
}
|
|
2377
|
-
function renderValue(field, value, draft, t) {
|
|
2934
|
+
function renderValue(field, value, draft, t, dictDisplayName) {
|
|
2378
2935
|
if (draft !== null) return `${draft}_`;
|
|
2379
2936
|
if (field.kind === "bool") return value ? `\u2713 ${t.common.on}` : `\u2717 ${t.common.off}`;
|
|
2380
|
-
if (field.kind === "dictRef")
|
|
2381
|
-
|
|
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
|
+
}
|
|
2382
2949
|
return String(value ?? "");
|
|
2383
2950
|
}
|
|
2384
2951
|
function hintFor(field, editing, t) {
|
|
@@ -2390,40 +2957,40 @@ function hintFor(field, editing, t) {
|
|
|
2390
2957
|
}
|
|
2391
2958
|
|
|
2392
2959
|
// src/ui/screens/StatsViewer.tsx
|
|
2393
|
-
import { useEffect as
|
|
2394
|
-
import { Box as
|
|
2395
|
-
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";
|
|
2396
2963
|
var DAY_WINDOWS = [7, 14, 30, 90];
|
|
2397
2964
|
function StatsViewer() {
|
|
2398
2965
|
const nav = useNav();
|
|
2399
2966
|
const t = useStrings();
|
|
2400
|
-
const [sessions, setSessions] =
|
|
2401
|
-
const [book, setBook] =
|
|
2402
|
-
const [windowIdx, setWindowIdx] =
|
|
2403
|
-
|
|
2967
|
+
const [sessions, setSessions] = useState12(null);
|
|
2968
|
+
const [book, setBook] = useState12(null);
|
|
2969
|
+
const [windowIdx, setWindowIdx] = useState12(1);
|
|
2970
|
+
useEffect8(() => {
|
|
2404
2971
|
void (async () => {
|
|
2405
2972
|
const [s, b] = await Promise.all([loadSessions(), loadMistakes()]);
|
|
2406
2973
|
setSessions(s);
|
|
2407
2974
|
setBook(b);
|
|
2408
2975
|
})();
|
|
2409
2976
|
}, []);
|
|
2410
|
-
|
|
2411
|
-
if (key.escape
|
|
2977
|
+
useInput7((_input, key) => {
|
|
2978
|
+
if (key.escape) {
|
|
2412
2979
|
nav.back();
|
|
2413
2980
|
return;
|
|
2414
2981
|
}
|
|
2415
|
-
if (
|
|
2416
|
-
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);
|
|
2417
2984
|
});
|
|
2418
2985
|
if (!sessions || !book) {
|
|
2419
|
-
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 }) });
|
|
2420
2987
|
}
|
|
2421
2988
|
if (sessions.length === 0) {
|
|
2422
|
-
return /* @__PURE__ */
|
|
2423
|
-
/* @__PURE__ */
|
|
2424
|
-
/* @__PURE__ */
|
|
2425
|
-
/* @__PURE__ */
|
|
2426
|
-
"
|
|
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 ",
|
|
2427
2994
|
t.common.back
|
|
2428
2995
|
] }) })
|
|
2429
2996
|
] });
|
|
@@ -2442,98 +3009,162 @@ function StatsViewer() {
|
|
|
2442
3009
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
2443
3010
|
const recent = sessions.slice(-5).reverse();
|
|
2444
3011
|
const top = topN(book, 8);
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
/* @__PURE__ */
|
|
2454
|
-
/* @__PURE__ */
|
|
2455
|
-
/* @__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 })
|
|
2456
3026
|
] })
|
|
2457
3027
|
] }),
|
|
2458
|
-
/* @__PURE__ */
|
|
2459
|
-
/* @__PURE__ */
|
|
2460
|
-
/* @__PURE__ */
|
|
2461
|
-
/* @__PURE__ */
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
/* @__PURE__ */
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
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
|
+
)
|
|
2477
3056
|
] })
|
|
2478
3057
|
] }),
|
|
2479
|
-
/* @__PURE__ */
|
|
2480
|
-
/* @__PURE__ */
|
|
2481
|
-
recent.map((s, i) => /* @__PURE__ */
|
|
2482
|
-
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2483
|
-
" ",
|
|
2484
|
-
s.ts.replace("T", " ").slice(0, 16),
|
|
2485
|
-
" "
|
|
2486
|
-
] }),
|
|
2487
|
-
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.text, children: s.dictId.padEnd(14) }),
|
|
2488
|
-
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2489
|
-
" ",
|
|
2490
|
-
"ch",
|
|
2491
|
-
String(s.chapter + 1).padStart(3),
|
|
2492
|
-
" ",
|
|
2493
|
-
s.mode.padEnd(9),
|
|
2494
|
-
" ",
|
|
2495
|
-
String(s.wordCount).padStart(3),
|
|
2496
|
-
"w ",
|
|
2497
|
-
s.errors,
|
|
2498
|
-
"err ",
|
|
2499
|
-
computeWPM(s),
|
|
2500
|
-
"wpm ",
|
|
2501
|
-
Math.round(accuracy(s) * 1e3) / 10,
|
|
2502
|
-
"%"
|
|
2503
|
-
] })
|
|
2504
|
-
] }, 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))
|
|
2505
3061
|
] }),
|
|
2506
|
-
top.length > 0 && /* @__PURE__ */
|
|
2507
|
-
/* @__PURE__ */
|
|
2508
|
-
top.map(([word, entry]) => /* @__PURE__ */
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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
|
+
" "
|
|
3108
|
+
] }),
|
|
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
|
+
" "
|
|
2517
3145
|
] }),
|
|
2518
|
-
/* @__PURE__ */
|
|
2519
|
-
/* @__PURE__ */
|
|
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
|
+
] })
|
|
2520
3151
|
] });
|
|
2521
3152
|
}
|
|
2522
3153
|
function Stat({ label, value, accent = false }) {
|
|
2523
|
-
return /* @__PURE__ */
|
|
2524
|
-
/* @__PURE__ */
|
|
3154
|
+
return /* @__PURE__ */ jsxs8(Box9, { marginRight: 3, children: [
|
|
3155
|
+
/* @__PURE__ */ jsxs8(Text8, { color: PALETTE.muted, children: [
|
|
2525
3156
|
label,
|
|
2526
3157
|
" "
|
|
2527
3158
|
] }),
|
|
2528
|
-
/* @__PURE__ */
|
|
3159
|
+
/* @__PURE__ */ jsx14(Text8, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
|
|
2529
3160
|
] });
|
|
2530
3161
|
}
|
|
2531
3162
|
|
|
2532
3163
|
// src/ui/screens/WordLookup.tsx
|
|
2533
|
-
import { useEffect as
|
|
2534
|
-
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";
|
|
2535
3166
|
import { readdir } from "fs/promises";
|
|
2536
|
-
import { Fragment as Fragment2, jsx as
|
|
3167
|
+
import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2537
3168
|
async function listLocalDictIds() {
|
|
2538
3169
|
try {
|
|
2539
3170
|
const files = await readdir(paths.dictsDir);
|
|
@@ -2545,12 +3176,12 @@ async function listLocalDictIds() {
|
|
|
2545
3176
|
function WordLookup() {
|
|
2546
3177
|
const nav = useNav();
|
|
2547
3178
|
const t = useStrings();
|
|
2548
|
-
const [query, setQuery] =
|
|
2549
|
-
const [allWords, setAllWords] =
|
|
2550
|
-
const [book, setBook] =
|
|
2551
|
-
const [loading, setLoading] =
|
|
2552
|
-
const [selected, setSelected] =
|
|
2553
|
-
|
|
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(() => {
|
|
2554
3185
|
void (async () => {
|
|
2555
3186
|
const ids = await listLocalDictIds();
|
|
2556
3187
|
const collected = [];
|
|
@@ -2566,7 +3197,7 @@ function WordLookup() {
|
|
|
2566
3197
|
}, []);
|
|
2567
3198
|
const q = query.toLowerCase().trim();
|
|
2568
3199
|
const filtered = q ? allWords.filter((h) => h.word.name.toLowerCase().includes(q)).slice(0, 50) : [];
|
|
2569
|
-
|
|
3200
|
+
useInput8((input, key) => {
|
|
2570
3201
|
if (key.escape) {
|
|
2571
3202
|
nav.back();
|
|
2572
3203
|
return;
|
|
@@ -2590,81 +3221,129 @@ function WordLookup() {
|
|
|
2590
3221
|
}
|
|
2591
3222
|
});
|
|
2592
3223
|
if (loading) {
|
|
2593
|
-
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 }) });
|
|
2594
3225
|
}
|
|
2595
3226
|
if (allWords.length === 0) {
|
|
2596
|
-
return /* @__PURE__ */
|
|
2597
|
-
/* @__PURE__ */
|
|
2598
|
-
/* @__PURE__ */
|
|
2599
|
-
/* @__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: [
|
|
2600
3231
|
"[Esc] ",
|
|
2601
3232
|
t.common.back
|
|
2602
3233
|
] }) })
|
|
2603
3234
|
] });
|
|
2604
3235
|
}
|
|
2605
3236
|
const current = filtered[selected];
|
|
2606
|
-
return /* @__PURE__ */
|
|
2607
|
-
/* @__PURE__ */
|
|
2608
|
-
/* @__PURE__ */
|
|
2609
|
-
/* @__PURE__ */
|
|
2610
|
-
/* @__PURE__ */
|
|
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) })
|
|
2611
3242
|
] }),
|
|
2612
|
-
/* @__PURE__ */
|
|
2613
|
-
/* @__PURE__ */
|
|
2614
|
-
/* @__PURE__ */
|
|
2615
|
-
/* @__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: "_" })
|
|
2616
3247
|
] }),
|
|
2617
|
-
/* @__PURE__ */
|
|
2618
|
-
/* @__PURE__ */
|
|
2619
|
-
filtered.map((h, i) => {
|
|
2620
|
-
|
|
2621
|
-
return /* @__PURE__ */ jsxs7(Box8, { children: [
|
|
2622
|
-
/* @__PURE__ */ jsx12(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2623
|
-
/* @__PURE__ */ jsx12(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: h.word.name.padEnd(20) }),
|
|
2624
|
-
/* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: h.dictId })
|
|
2625
|
-
] }, `${h.dictId}-${h.word.name}-${i}`);
|
|
2626
|
-
}),
|
|
2627
|
-
filtered.length === 0 && q && /* @__PURE__ */ jsx12(Text7, { color: PALETTE.muted, children: t.word.noMatches(query) })
|
|
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) })
|
|
2628
3252
|
] }),
|
|
2629
|
-
/* @__PURE__ */
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
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
|
+
] })
|
|
2650
3282
|
] }),
|
|
2651
|
-
/* @__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 }) })
|
|
2652
3331
|
] });
|
|
2653
3332
|
}
|
|
2654
3333
|
|
|
2655
3334
|
// src/ui/App.tsx
|
|
2656
|
-
import { jsx as
|
|
3335
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2657
3336
|
function App({ initial, initialCfg }) {
|
|
2658
|
-
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, {}) }) }) }) }) }) });
|
|
2659
3338
|
}
|
|
2660
3339
|
function LangBridge({ children }) {
|
|
2661
3340
|
const { cfg } = useAppState();
|
|
2662
|
-
return /* @__PURE__ */
|
|
3341
|
+
return /* @__PURE__ */ jsx17(StringsProvider, { pref: cfg.language, children });
|
|
2663
3342
|
}
|
|
2664
3343
|
function screenKey(frame) {
|
|
2665
3344
|
if (frame.name === "practice") {
|
|
2666
3345
|
const p = frame.params;
|
|
2667
|
-
return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}`;
|
|
3346
|
+
return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? "s" : "n"}`;
|
|
2668
3347
|
}
|
|
2669
3348
|
return frame.name;
|
|
2670
3349
|
}
|
|
@@ -2673,7 +3352,7 @@ function Router() {
|
|
|
2673
3352
|
const { cfg } = useAppState();
|
|
2674
3353
|
const { exit } = useApp4();
|
|
2675
3354
|
const lastKeyRef = useRef4(null);
|
|
2676
|
-
|
|
3355
|
+
useInput10((input, key2) => {
|
|
2677
3356
|
if (key2.ctrl && input === "c") exit();
|
|
2678
3357
|
});
|
|
2679
3358
|
const frame = nav.current;
|
|
@@ -2684,17 +3363,19 @@ function Router() {
|
|
|
2684
3363
|
}
|
|
2685
3364
|
switch (frame.name) {
|
|
2686
3365
|
case "main":
|
|
2687
|
-
return /* @__PURE__ */
|
|
3366
|
+
return /* @__PURE__ */ jsx17(MainMenu, { cfg });
|
|
2688
3367
|
case "practice":
|
|
2689
|
-
return /* @__PURE__ */
|
|
3368
|
+
return /* @__PURE__ */ jsx17(PracticeScreen, { params: frame.params });
|
|
2690
3369
|
case "dict":
|
|
2691
|
-
return /* @__PURE__ */
|
|
3370
|
+
return /* @__PURE__ */ jsx17(DictBrowser, { params: frame.params });
|
|
2692
3371
|
case "config":
|
|
2693
|
-
return /* @__PURE__ */
|
|
3372
|
+
return /* @__PURE__ */ jsx17(ConfigEditor, {});
|
|
2694
3373
|
case "stats":
|
|
2695
|
-
return /* @__PURE__ */
|
|
3374
|
+
return /* @__PURE__ */ jsx17(StatsViewer, {});
|
|
2696
3375
|
case "word":
|
|
2697
|
-
return /* @__PURE__ */
|
|
3376
|
+
return /* @__PURE__ */ jsx17(WordLookup, {});
|
|
3377
|
+
case "help":
|
|
3378
|
+
return /* @__PURE__ */ jsx17(HelpScreen, {});
|
|
2698
3379
|
}
|
|
2699
3380
|
}
|
|
2700
3381
|
|
|
@@ -2716,7 +3397,19 @@ function fmtDuration(ms, lang) {
|
|
|
2716
3397
|
return `${m}m ${s}s`;
|
|
2717
3398
|
}
|
|
2718
3399
|
function printSessionReport(r, t, lang) {
|
|
2719
|
-
if (r.chaptersCompleted === 0) return;
|
|
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
|
+
}
|
|
2720
3413
|
const accPct = Math.round(r.accuracy * 1e3) / 10;
|
|
2721
3414
|
const labels = [
|
|
2722
3415
|
t.report.duration,
|
|
@@ -2770,10 +3463,11 @@ async function runPractice(dictIdArg, options) {
|
|
|
2770
3463
|
return;
|
|
2771
3464
|
}
|
|
2772
3465
|
const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);
|
|
3466
|
+
const stealth = options.stealth === true || cfg.stealth === "default";
|
|
2773
3467
|
start();
|
|
2774
3468
|
const { waitUntilExit } = render(
|
|
2775
3469
|
createElement(App, {
|
|
2776
|
-
initial: { name: "practice", params: { dictId, chapterIndex, mode } },
|
|
3470
|
+
initial: { name: "practice", params: { dictId, chapterIndex, mode, stealth } },
|
|
2777
3471
|
initialCfg: cfg
|
|
2778
3472
|
}),
|
|
2779
3473
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
@@ -2784,9 +3478,11 @@ async function runPractice(dictIdArg, options) {
|
|
|
2784
3478
|
printSessionReport(report(), t, lang);
|
|
2785
3479
|
}
|
|
2786
3480
|
function buildPracticeCommand() {
|
|
2787
|
-
return new Command3("practice").argument("[dictId]", "dictionary id; falls back to config.defaultDict").description("Start a typing practice session").option("-c, --chapter <n>", "chapter number (1-based)", "1").option("-m, --mode <mode>", "order | dictation | review | random | loop").
|
|
2788
|
-
|
|
2789
|
-
|
|
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
|
+
);
|
|
2790
3486
|
}
|
|
2791
3487
|
|
|
2792
3488
|
// src/commands/stats.ts
|
|
@@ -2910,6 +3606,16 @@ function buildWordCommand() {
|
|
|
2910
3606
|
});
|
|
2911
3607
|
}
|
|
2912
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
|
+
|
|
2913
3619
|
// src/commands/menu.ts
|
|
2914
3620
|
import { render as render2 } from "ink";
|
|
2915
3621
|
import { createElement as createElement2 } from "react";
|
|
@@ -2931,9 +3637,10 @@ async function runMainMenu() {
|
|
|
2931
3637
|
}
|
|
2932
3638
|
|
|
2933
3639
|
// src/cli.ts
|
|
2934
|
-
var program = new
|
|
3640
|
+
var program = new Command7();
|
|
2935
3641
|
program.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version(package_default.version);
|
|
2936
3642
|
program.addCommand(buildPracticeCommand());
|
|
3643
|
+
program.addCommand(buildStealthCommand());
|
|
2937
3644
|
program.addCommand(buildDictCommand());
|
|
2938
3645
|
program.addCommand(buildWordCommand());
|
|
2939
3646
|
program.addCommand(buildStatsCommand());
|