qwerty-cli 0.0.1-alpha.6 → 0.0.1-alpha.8
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 +1258 -472
- 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.8",
|
|
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,52 @@ 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
|
+
pausedHintRight: "Enter resume",
|
|
1022
|
+
nextHintRight: "Enter next",
|
|
1023
|
+
infoChipLabel: "info",
|
|
1024
|
+
infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
|
|
948
1025
|
}
|
|
949
1026
|
};
|
|
950
1027
|
var zh = {
|
|
@@ -956,12 +1033,13 @@ var zh = {
|
|
|
956
1033
|
back: "\u8FD4\u56DE",
|
|
957
1034
|
quit: "\u9000\u51FA",
|
|
958
1035
|
on: "\u5F00",
|
|
959
|
-
off: "\u5173"
|
|
1036
|
+
off: "\u5173",
|
|
1037
|
+
cancel: "\u53D6\u6D88"
|
|
960
1038
|
},
|
|
961
1039
|
mainMenu: {
|
|
962
1040
|
items: {
|
|
963
1041
|
practiceLabel: "\u7EC3\u4E60",
|
|
964
|
-
practiceHintWith: (
|
|
1042
|
+
practiceHintWith: (name) => `\u5F00\u59CB ${name}`,
|
|
965
1043
|
practiceHintNone: "\u8BF7\u5148\u9009\u8BCD\u5178",
|
|
966
1044
|
dictLabel: "\u8BCD\u5178",
|
|
967
1045
|
dictHint: "\u6D4F\u89C8\u3001\u4E0B\u8F7D\u3001\u8BBE\u4E3A\u9ED8\u8BA4",
|
|
@@ -971,18 +1049,19 @@ var zh = {
|
|
|
971
1049
|
statsHint: "\u5386\u53F2\u4E0E\u8D8B\u52BF",
|
|
972
1050
|
configLabel: "\u8BBE\u7F6E",
|
|
973
1051
|
configHint: "\u4FEE\u6539\u504F\u597D",
|
|
1052
|
+
stealthLabel: "\u6478\u9C7C",
|
|
1053
|
+
stealthHint: "\u5B89\u9759\u7EC3\u4E60\u6A21\u5F0F",
|
|
974
1054
|
quitLabel: "\u9000\u51FA",
|
|
975
|
-
quitHint: "Ctrl+C \
|
|
1055
|
+
quitHint: "Esc \u6216 Ctrl+C \u9000\u51FA"
|
|
976
1056
|
},
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE"
|
|
1057
|
+
hint: "\u2191/\u2193 \u79FB\u52A8 \xB7 Enter \u786E\u8BA4 \xB7 \u5B57\u6BCD\u76F4\u8FBE",
|
|
1058
|
+
helpHint: "? \u5E2E\u52A9"
|
|
980
1059
|
},
|
|
981
1060
|
dict: {
|
|
982
1061
|
title: "\u8BCD\u5178",
|
|
983
1062
|
loading: "\u52A0\u8F7D\u8BCD\u5178\u4E2D\u2026",
|
|
984
1063
|
entries: (n) => `${n} \u90E8\u8BCD\u5178`,
|
|
985
|
-
|
|
1064
|
+
filterPlaceholder: "\u8F93\u5165\u8FC7\u6EE4",
|
|
986
1065
|
local: "\u5DF2\u4E0B\u8F7D \u2713",
|
|
987
1066
|
notLocal: "\u672A\u4E0B\u8F7D",
|
|
988
1067
|
defaultMark: "\u9ED8\u8BA4 \u2605",
|
|
@@ -991,7 +1070,19 @@ var zh = {
|
|
|
991
1070
|
pulling: (id) => `\u62C9\u53D6 ${id} \u4E2D\u2026`,
|
|
992
1071
|
removing: (id) => `\u5220\u9664 ${id} \u4E2D\u2026`,
|
|
993
1072
|
errorOn: (id, msg) => `${id} \u51FA\u9519:${msg}`,
|
|
994
|
-
footer: "\u2191/\u2193 \u9009\u62E9 \xB7 Enter \
|
|
1073
|
+
footer: "\u2191/\u2193 \u9009\u62E9 \xB7 Enter \u64CD\u4F5C \xB7 Ctrl+K \u66F4\u591A \xB7 Esc \u8FD4\u56DE",
|
|
1074
|
+
action: {
|
|
1075
|
+
title: "\u5F53\u524D\u8BCD\u5178",
|
|
1076
|
+
setDefault: "\u8BBE\u4E3A\u9ED8\u8BA4",
|
|
1077
|
+
practice: "\u7ACB\u5373\u7EC3\u4E60",
|
|
1078
|
+
delete: "\u5220\u9664\u672C\u5730"
|
|
1079
|
+
},
|
|
1080
|
+
command: {
|
|
1081
|
+
title: "\u66F4\u591A\u529F\u80FD",
|
|
1082
|
+
pull: "\u62C9\u53D6\u9009\u4E2D",
|
|
1083
|
+
import: "\u5BFC\u5165 .json",
|
|
1084
|
+
refreshList: "\u66F4\u65B0\u8BCD\u5178\u5217\u8868"
|
|
1085
|
+
}
|
|
995
1086
|
},
|
|
996
1087
|
config: {
|
|
997
1088
|
title: "\u8BBE\u7F6E",
|
|
@@ -999,14 +1090,18 @@ var zh = {
|
|
|
999
1090
|
defaultDict: "\u9ED8\u8BA4\u8BCD\u5178",
|
|
1000
1091
|
defaultMode: "\u9ED8\u8BA4\u6A21\u5F0F",
|
|
1001
1092
|
accent: "\u53D1\u97F3",
|
|
1002
|
-
mirror: "\u955C\u50CF\u6E90",
|
|
1093
|
+
mirror: "\u8BCD\u5178\u955C\u50CF\u6E90",
|
|
1003
1094
|
chapterSize: "\u7AE0\u8282\u5355\u8BCD\u6570",
|
|
1004
1095
|
autoplayPronunciation: "\u81EA\u52A8\u64AD\u653E\u53D1\u97F3",
|
|
1005
1096
|
soundsMaster: "\u97F3\u6548\u603B\u5F00\u5173",
|
|
1006
1097
|
soundsKeystroke: "\u6309\u952E\u97F3",
|
|
1007
1098
|
soundsFeedback: "\u53CD\u9988\u97F3",
|
|
1008
1099
|
soundsKeySound: "\u6309\u952E\u97F3\u8272",
|
|
1009
|
-
language: "\u8BED\u8A00"
|
|
1100
|
+
language: "\u8BED\u8A00",
|
|
1101
|
+
stealth: "\u6478\u9C7C\u6A21\u5F0F"
|
|
1102
|
+
},
|
|
1103
|
+
enumValues: {
|
|
1104
|
+
stealth: { off: "\u5173\u95ED", menu: "\u4E3B\u83DC\u5355\u663E\u793A", default: "\u9ED8\u8BA4\u7EC3\u4E60\u6A21\u5F0F" }
|
|
1010
1105
|
},
|
|
1011
1106
|
hints: {
|
|
1012
1107
|
editing: "\u8F93\u5165\u4FEE\u6539 \xB7 Enter \u4FDD\u5B58 \xB7 Esc \u53D6\u6D88",
|
|
@@ -1017,7 +1112,7 @@ var zh = {
|
|
|
1017
1112
|
}
|
|
1018
1113
|
},
|
|
1019
1114
|
stats: {
|
|
1020
|
-
title: "\u7EDF\u8BA1",
|
|
1115
|
+
title: "\u7EDF\u8BA1 \xB7 \u6982\u89C8",
|
|
1021
1116
|
loading: "\u52A0\u8F7D\u7EDF\u8BA1\u4E2D\u2026",
|
|
1022
1117
|
none: "\u8FD8\u6CA1\u6709\u7EC3\u4E60\u8BB0\u5F55\u3002",
|
|
1023
1118
|
nonePractice: "\u5148\u6765\u4E00\u6B21\u7EC3\u4E60\u5427\u3002",
|
|
@@ -1028,11 +1123,14 @@ var zh = {
|
|
|
1028
1123
|
wpm: "\u901F\u5EA6",
|
|
1029
1124
|
accuracy: "\u51C6\u786E\u7387",
|
|
1030
1125
|
streak: "\u8FDE\u7EED\u5929\u6570",
|
|
1031
|
-
last: (n) => `\u6700\u8FD1 ${n} \u5929 (
|
|
1032
|
-
cycleWindow: "
|
|
1126
|
+
last: (n) => `\u6700\u8FD1 ${n} \u5929 (\u2190/\u2192 \u5207\u6362\u7A97\u53E3)`,
|
|
1127
|
+
cycleWindow: "\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",
|
|
1033
1128
|
recent: "\u6700\u8FD1\u4F1A\u8BDD",
|
|
1034
1129
|
topMistakes: "\u9AD8\u9891\u9519\u8BCD",
|
|
1035
|
-
footer: "
|
|
1130
|
+
footer: "\u2190/\u2192 \u5207\u6362\u7A97\u53E3 \xB7 Esc \u8FD4\u56DE",
|
|
1131
|
+
maxLabel: "\u6700\u5927",
|
|
1132
|
+
recentUnits: { words: "\u8BCD", errors: "\u9519", wpm: "\u901F" },
|
|
1133
|
+
multiDictSuffix: (n) => ` \u7B49 ${n} \u90E8`
|
|
1036
1134
|
},
|
|
1037
1135
|
word: {
|
|
1038
1136
|
title: "\u67E5\u8BCD",
|
|
@@ -1041,7 +1139,7 @@ var zh = {
|
|
|
1041
1139
|
pullFirst: "\u5148\u5728\u300C\u8BCD\u5178\u300D\u4E2D\u62C9\u53D6\u4E00\u90E8\u3002",
|
|
1042
1140
|
countAcross: (n) => `\u672C\u5730\u8BCD\u5178\u5171 ${n} \u8BCD`,
|
|
1043
1141
|
noMatches: (q) => `\u6CA1\u6709\u5339\u914D\u300C${q}\u300D\u7684\u8BCD`,
|
|
1044
|
-
inDict: (
|
|
1142
|
+
inDict: (name) => `\u6765\u6E90:${name}`,
|
|
1045
1143
|
mistakes: (n, date) => `\u9519\u8FC7 ${n} \u6B21 (\u6700\u8FD1 ${date})`,
|
|
1046
1144
|
footer: "\u8F93\u5165\u8FC7\u6EE4 \xB7 \u2191/\u2193 \u9009\u62E9 \xB7 Esc \u8FD4\u56DE"
|
|
1047
1145
|
},
|
|
@@ -1073,10 +1171,20 @@ var zh = {
|
|
|
1073
1171
|
accuracy: "\u51C6\u786E\u7387",
|
|
1074
1172
|
elapsed: (t) => `\u8017\u65F6 ${t}`
|
|
1075
1173
|
},
|
|
1174
|
+
pause: {
|
|
1175
|
+
title: "\u5DF2\u6682\u505C",
|
|
1176
|
+
chapter: (c, t) => `\u7B2C ${c}/${t} \u7AE0`,
|
|
1177
|
+
progress: (completed, total) => `${completed}/${total}`,
|
|
1178
|
+
hint: "Enter \u7EE7\u7EED \xB7 Esc \u8FD4\u56DE\u83DC\u5355"
|
|
1179
|
+
},
|
|
1180
|
+
summary: {
|
|
1181
|
+
loopAgain: "\u518D\u6765\u4E00\u904D",
|
|
1182
|
+
nextChapter: "\u4E0B\u4E00\u7AE0",
|
|
1183
|
+
reviewMistakes: "\u590D\u4E60\u9519\u8BCD",
|
|
1184
|
+
backMenu: "\u8FD4\u56DE\u83DC\u5355"
|
|
1185
|
+
},
|
|
1076
1186
|
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"
|
|
1187
|
+
typing: "Ctrl+N \u8DF3\u8FC7 \xB7 Esc \u6682\u505C \xB7 Tab \u91CD\u64AD"
|
|
1080
1188
|
},
|
|
1081
1189
|
errors: {
|
|
1082
1190
|
noMistakes: "\u9519\u8BCD\u672C\u662F\u7A7A\u7684\u3002\u5148\u7EC3\u4E60\u51E0\u7AE0\u5427\u3002",
|
|
@@ -1096,7 +1204,52 @@ var zh = {
|
|
|
1096
1204
|
accuracy: "\u51C6\u786E\u7387",
|
|
1097
1205
|
wpm: "\u901F\u5EA6",
|
|
1098
1206
|
newMistakes: "\u65B0\u9519\u8BCD",
|
|
1099
|
-
farewell: "\u4E0B\u6B21\u89C1\u3002"
|
|
1207
|
+
farewell: "\u4E0B\u6B21\u89C1\u3002",
|
|
1208
|
+
notPracticed: "\u672C\u6B21\u672A\u7EC3\u4E60"
|
|
1209
|
+
},
|
|
1210
|
+
help: {
|
|
1211
|
+
title: "\u5E2E\u52A9",
|
|
1212
|
+
subtitle: "\u5168\u90E8\u5FEB\u6377\u952E",
|
|
1213
|
+
sections: {
|
|
1214
|
+
main: "\u4E3B\u83DC\u5355",
|
|
1215
|
+
practice: "\u7EC3\u4E60",
|
|
1216
|
+
dict: "\u8BCD\u5178",
|
|
1217
|
+
config: "\u8BBE\u7F6E",
|
|
1218
|
+
stats: "\u7EDF\u8BA1",
|
|
1219
|
+
word: "\u67E5\u8BCD",
|
|
1220
|
+
global: "\u5168\u5C40"
|
|
1221
|
+
},
|
|
1222
|
+
keys: {
|
|
1223
|
+
navigate: "\u2191/\u2193 \u79FB\u52A8\u9009\u9879",
|
|
1224
|
+
select: "Enter \u786E\u8BA4 / \u7EE7\u7EED",
|
|
1225
|
+
letterJump: "\u5B57\u6BCD\u952E \u76F4\u8FBE\u83DC\u5355\u9879",
|
|
1226
|
+
pause: "Esc \u6682\u505C\u7EC3\u4E60",
|
|
1227
|
+
skip: "Ctrl+N \u8DF3\u8FC7\u5F53\u524D\u8BCD(\u4E0D\u8BA1\u9519)",
|
|
1228
|
+
replay: "Tab \u91CD\u64AD\u53D1\u97F3",
|
|
1229
|
+
resume: "Enter \u7EE7\u7EED\u7EC3\u4E60",
|
|
1230
|
+
backMenu: "Esc \u8FD4\u56DE\u4E0A\u4E00\u5C4F",
|
|
1231
|
+
backScreen: "Esc \u5173\u95ED\u9762\u677F / \u8FD4\u56DE",
|
|
1232
|
+
nextChapter: "Enter \u4E0B\u4E00\u7AE0",
|
|
1233
|
+
reviewMistakes: "m \u590D\u4E60\u9519\u8BCD",
|
|
1234
|
+
filter: "\u8F93\u5165 \u8FC7\u6EE4\u5217\u8868",
|
|
1235
|
+
itemActions: "Enter \u5F39\u51FA\u52A8\u4F5C\u9762\u677F",
|
|
1236
|
+
moreActions: "Ctrl+K \u5F39\u51FA\u66F4\u591A\u529F\u80FD",
|
|
1237
|
+
cycleWindow: "\u2190/\u2192 \u5207\u6362\u65E5\u7A97\u53E3",
|
|
1238
|
+
stealthToggle: "Ctrl+I \u5207\u6362\u6478\u9C7C\u4FE1\u606F\u884C",
|
|
1239
|
+
helpScreen: "? \u6253\u5F00\u672C\u5E2E\u52A9\u9875",
|
|
1240
|
+
quit: "Ctrl+C \u7ACB\u5373\u9000\u51FA"
|
|
1241
|
+
},
|
|
1242
|
+
footer: "Esc \u8FD4\u56DE"
|
|
1243
|
+
},
|
|
1244
|
+
stealth: {
|
|
1245
|
+
paused: "paused",
|
|
1246
|
+
chapterDone: "chapter done",
|
|
1247
|
+
resumeHint: "Enter resume \xB7 Esc menu",
|
|
1248
|
+
nextHint: "Enter next \xB7 Esc menu",
|
|
1249
|
+
pausedHintRight: "Enter \u7EE7\u7EED",
|
|
1250
|
+
nextHintRight: "Enter \u4E0B\u4E00\u7AE0",
|
|
1251
|
+
infoChipLabel: "\u4FE1\u606F",
|
|
1252
|
+
infoFmt: (dict, chapter, completed, total, wpm, accPct) => `${dict} \xB7 ${chapter} \xB7 ${completed}/${total} \xB7 ${wpm} wpm \xB7 ${accPct}%`
|
|
1100
1253
|
}
|
|
1101
1254
|
};
|
|
1102
1255
|
|
|
@@ -1145,13 +1298,55 @@ function pickStrings(pref) {
|
|
|
1145
1298
|
return { lang, t: lang === "zh" ? zh : en };
|
|
1146
1299
|
}
|
|
1147
1300
|
|
|
1301
|
+
// src/ui/registry-context.tsx
|
|
1302
|
+
import { createContext as createContext5, useContext as useContext5, useEffect as useEffect3, useState as useState5 } from "react";
|
|
1303
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1304
|
+
var RegistryContext = createContext5(null);
|
|
1305
|
+
function RegistryProvider({ children }) {
|
|
1306
|
+
const [registry, setRegistry] = useState5(null);
|
|
1307
|
+
const [byId, setById] = useState5(/* @__PURE__ */ new Map());
|
|
1308
|
+
useEffect3(() => {
|
|
1309
|
+
let cancelled = false;
|
|
1310
|
+
(async () => {
|
|
1311
|
+
try {
|
|
1312
|
+
const reg = await loadRegistry();
|
|
1313
|
+
if (cancelled) return;
|
|
1314
|
+
const map = /* @__PURE__ */ new Map();
|
|
1315
|
+
for (const e of reg) map.set(e.id, e);
|
|
1316
|
+
setRegistry(reg);
|
|
1317
|
+
setById(map);
|
|
1318
|
+
} catch {
|
|
1319
|
+
if (!cancelled) {
|
|
1320
|
+
setRegistry([]);
|
|
1321
|
+
setById(/* @__PURE__ */ new Map());
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
})();
|
|
1325
|
+
return () => {
|
|
1326
|
+
cancelled = true;
|
|
1327
|
+
};
|
|
1328
|
+
}, []);
|
|
1329
|
+
return /* @__PURE__ */ jsx6(RegistryContext.Provider, { value: { registry, byId }, children });
|
|
1330
|
+
}
|
|
1331
|
+
function useRegistry() {
|
|
1332
|
+
const ctx = useContext5(RegistryContext);
|
|
1333
|
+
if (!ctx) throw new Error("useRegistry must be used inside RegistryProvider");
|
|
1334
|
+
return ctx;
|
|
1335
|
+
}
|
|
1336
|
+
function useDictName(id) {
|
|
1337
|
+
const { byId } = useRegistry();
|
|
1338
|
+
if (!id) return "";
|
|
1339
|
+
const entry = byId.get(id);
|
|
1340
|
+
return entry?.name ?? id;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1148
1343
|
// src/ui/screens/MainMenu.tsx
|
|
1149
|
-
import { useState as
|
|
1344
|
+
import { useState as useState6 } from "react";
|
|
1150
1345
|
import { Box as Box3, Text as Text2, useApp, useInput } from "ink";
|
|
1151
1346
|
|
|
1152
1347
|
// src/ui/components/BigWord.tsx
|
|
1153
|
-
import { Box as Box2, Text
|
|
1154
|
-
import { jsx as
|
|
1348
|
+
import { Box as Box2, Text } from "ink";
|
|
1349
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1155
1350
|
var PALETTE = {
|
|
1156
1351
|
accent: "#5eead4",
|
|
1157
1352
|
muted: "#6b7280",
|
|
@@ -1162,28 +1357,15 @@ var PALETTE = {
|
|
|
1162
1357
|
error: "#f87171"
|
|
1163
1358
|
};
|
|
1164
1359
|
function BigWord({ target, typed, error = false, hideTarget = false }) {
|
|
1165
|
-
const { stdout } = useStdout2();
|
|
1166
|
-
const cols = stdout?.columns ?? 80;
|
|
1167
1360
|
const chars = [...target];
|
|
1168
1361
|
const typedChars = [...typed];
|
|
1169
|
-
|
|
1170
|
-
return /* @__PURE__ */ jsx6(Box2, { justifyContent: "center", children: chars.map((ch, i) => {
|
|
1362
|
+
return /* @__PURE__ */ jsx7(Box2, { paddingY: 4, justifyContent: "center", children: chars.map((ch, i) => {
|
|
1171
1363
|
const isTyped = i < typedChars.length;
|
|
1172
1364
|
const display = hideTarget && !isTyped ? "_" : isTyped ? typedChars[i] : ch;
|
|
1173
|
-
const color =
|
|
1174
|
-
return /* @__PURE__ */
|
|
1175
|
-
display,
|
|
1176
|
-
i < chars.length - 1 ? sep : ""
|
|
1177
|
-
] }, i);
|
|
1365
|
+
const color = error ? PALETTE.error : isTyped ? PALETTE.accent : PALETTE.muted;
|
|
1366
|
+
return /* @__PURE__ */ jsx7(Text, { bold: true, color, children: display }, i);
|
|
1178
1367
|
}) });
|
|
1179
1368
|
}
|
|
1180
|
-
function BoldSpaced({ text, color = PALETTE.text }) {
|
|
1181
|
-
const chars = [...text];
|
|
1182
|
-
return /* @__PURE__ */ jsx6(Box2, { children: chars.map((ch, i) => /* @__PURE__ */ jsxs(Text, { bold: true, color, children: [
|
|
1183
|
-
ch,
|
|
1184
|
-
i < chars.length - 1 ? " " : ""
|
|
1185
|
-
] }, i)) });
|
|
1186
|
-
}
|
|
1187
1369
|
|
|
1188
1370
|
// src/util/text.ts
|
|
1189
1371
|
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
@@ -1200,45 +1382,85 @@ function visibleWidth2(s) {
|
|
|
1200
1382
|
return w;
|
|
1201
1383
|
}
|
|
1202
1384
|
|
|
1385
|
+
// src/util/dict-name.ts
|
|
1386
|
+
function truncateName(name, max) {
|
|
1387
|
+
if (visibleWidth2(name) <= max) return name;
|
|
1388
|
+
let out = "";
|
|
1389
|
+
let w = 0;
|
|
1390
|
+
for (const ch of name) {
|
|
1391
|
+
const code = ch.codePointAt(0);
|
|
1392
|
+
const cw = code > 11904 && code < 64256 ? 2 : 1;
|
|
1393
|
+
if (w + cw > max - 1) break;
|
|
1394
|
+
out += ch;
|
|
1395
|
+
w += cw;
|
|
1396
|
+
}
|
|
1397
|
+
return out + "\u2026";
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1203
1400
|
// src/ui/screens/MainMenu.tsx
|
|
1204
|
-
import { jsx as
|
|
1401
|
+
import { jsx as jsx8, jsxs } from "react/jsx-runtime";
|
|
1205
1402
|
function MainMenu({ cfg }) {
|
|
1206
|
-
const [selected, setSelected] =
|
|
1403
|
+
const [selected, setSelected] = useState6(0);
|
|
1207
1404
|
const { exit } = useApp();
|
|
1208
1405
|
const nav = useNav();
|
|
1209
1406
|
const audio = useAudioStatus();
|
|
1210
1407
|
const t = useStrings();
|
|
1408
|
+
const defaultDictName = useDictName(cfg.defaultDict);
|
|
1211
1409
|
const m = t.mainMenu.items;
|
|
1410
|
+
const startPractice = (stealth) => {
|
|
1411
|
+
if (cfg.defaultDict) {
|
|
1412
|
+
nav.navigate({
|
|
1413
|
+
name: "practice",
|
|
1414
|
+
params: {
|
|
1415
|
+
dictId: cfg.defaultDict,
|
|
1416
|
+
chapterIndex: 0,
|
|
1417
|
+
mode: cfg.defaultMode,
|
|
1418
|
+
stealth
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
} else {
|
|
1422
|
+
nav.navigate({ name: "dict", params: { pickerMode: "choose-then-practice" } });
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1212
1425
|
const items = [
|
|
1213
1426
|
{
|
|
1214
1427
|
key: "p",
|
|
1215
1428
|
label: m.practiceLabel,
|
|
1216
|
-
hint: cfg.defaultDict ? m.practiceHintWith(
|
|
1217
|
-
run: () =>
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1429
|
+
hint: cfg.defaultDict ? m.practiceHintWith(truncateName(defaultDictName, 24)) : m.practiceHintNone,
|
|
1430
|
+
run: () => startPractice(cfg.stealth === "default")
|
|
1431
|
+
}
|
|
1432
|
+
];
|
|
1433
|
+
if (cfg.stealth === "menu" || cfg.stealth === "default") {
|
|
1434
|
+
items.push({
|
|
1435
|
+
key: "b",
|
|
1436
|
+
label: m.stealthLabel,
|
|
1437
|
+
hint: m.stealthHint,
|
|
1438
|
+
run: () => startPractice(true)
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
items.push(
|
|
1228
1442
|
{ key: "d", label: m.dictLabel, hint: m.dictHint, run: () => nav.navigate({ name: "dict" }) },
|
|
1229
1443
|
{ key: "w", label: m.wordLabel, hint: m.wordHint, run: () => nav.navigate({ name: "word" }) },
|
|
1230
1444
|
{ key: "s", label: m.statsLabel, hint: m.statsHint, run: () => nav.navigate({ name: "stats" }) },
|
|
1231
1445
|
{ key: "c", label: m.configLabel, hint: m.configHint, run: () => nav.navigate({ name: "config" }) },
|
|
1232
1446
|
{ key: "q", label: m.quitLabel, hint: m.quitHint, run: () => exit() }
|
|
1233
|
-
|
|
1447
|
+
);
|
|
1234
1448
|
const labelW = Math.max(...items.map((it) => visibleWidth2(it.label))) + 4;
|
|
1235
1449
|
useInput((input, key) => {
|
|
1450
|
+
if (key.escape) {
|
|
1451
|
+
exit();
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1236
1454
|
if (key.upArrow) setSelected((i) => (i - 1 + items.length) % items.length);
|
|
1237
1455
|
if (key.downArrow) setSelected((i) => (i + 1) % items.length);
|
|
1238
1456
|
if (key.return) {
|
|
1239
1457
|
items[selected].run();
|
|
1240
1458
|
return;
|
|
1241
1459
|
}
|
|
1460
|
+
if (input === "?") {
|
|
1461
|
+
nav.navigate({ name: "help" });
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1242
1464
|
for (const it of items) {
|
|
1243
1465
|
if (input === it.key) {
|
|
1244
1466
|
it.run();
|
|
@@ -1246,41 +1468,44 @@ function MainMenu({ cfg }) {
|
|
|
1246
1468
|
}
|
|
1247
1469
|
}
|
|
1248
1470
|
});
|
|
1249
|
-
return /* @__PURE__ */
|
|
1250
|
-
/* @__PURE__ */
|
|
1251
|
-
/* @__PURE__ */
|
|
1252
|
-
/* @__PURE__ */
|
|
1471
|
+
return /* @__PURE__ */ jsxs(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", children: [
|
|
1472
|
+
/* @__PURE__ */ jsxs(Box3, { children: [
|
|
1473
|
+
/* @__PURE__ */ jsx8(Text2, { bold: true, color: PALETTE.accent, children: t.app.title }),
|
|
1474
|
+
/* @__PURE__ */ jsxs(Text2, { color: PALETTE.muted, children: [
|
|
1253
1475
|
" \xB7 ",
|
|
1254
1476
|
t.app.subtitle
|
|
1255
1477
|
] })
|
|
1256
1478
|
] }),
|
|
1257
|
-
/* @__PURE__ */
|
|
1479
|
+
/* @__PURE__ */ jsx8(Box3, { marginTop: 2, flexDirection: "column", children: items.map((it, i) => {
|
|
1258
1480
|
const active = i === selected;
|
|
1259
1481
|
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(it.label)));
|
|
1260
|
-
return /* @__PURE__ */
|
|
1261
|
-
/* @__PURE__ */
|
|
1262
|
-
/* @__PURE__ */
|
|
1482
|
+
return /* @__PURE__ */ jsxs(Box3, { children: [
|
|
1483
|
+
/* @__PURE__ */ jsx8(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
1484
|
+
/* @__PURE__ */ jsxs(Text2, { color: active ? PALETTE.accent : PALETTE.muted, children: [
|
|
1263
1485
|
"[",
|
|
1264
1486
|
it.key,
|
|
1265
1487
|
"]"
|
|
1266
1488
|
] }),
|
|
1267
|
-
/* @__PURE__ */
|
|
1268
|
-
/* @__PURE__ */
|
|
1489
|
+
/* @__PURE__ */ jsx8(Text2, { children: " " }),
|
|
1490
|
+
/* @__PURE__ */ jsxs(Text2, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
|
|
1269
1491
|
it.label,
|
|
1270
1492
|
pad
|
|
1271
1493
|
] }),
|
|
1272
|
-
/* @__PURE__ */
|
|
1494
|
+
/* @__PURE__ */ jsx8(Text2, { color: PALETTE.muted, children: it.hint })
|
|
1273
1495
|
] }, it.key);
|
|
1274
1496
|
}) }),
|
|
1275
|
-
/* @__PURE__ */
|
|
1276
|
-
|
|
1277
|
-
|
|
1497
|
+
/* @__PURE__ */ jsx8(Box3, { marginTop: 2, children: /* @__PURE__ */ jsxs(Text2, { color: PALETTE.muted, children: [
|
|
1498
|
+
t.mainMenu.hint,
|
|
1499
|
+
" \xB7 ",
|
|
1500
|
+
t.mainMenu.helpHint
|
|
1501
|
+
] }) }),
|
|
1502
|
+
audio.warning && /* @__PURE__ */ jsx8(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text2, { color: PALETTE.warning, children: t.audio.noPlayer }) })
|
|
1278
1503
|
] });
|
|
1279
1504
|
}
|
|
1280
1505
|
|
|
1281
1506
|
// src/ui/screens/PracticeScreen.tsx
|
|
1282
|
-
import { useState as
|
|
1283
|
-
import { Box as
|
|
1507
|
+
import { useState as useState8, useEffect as useEffect6, useRef as useRef3 } from "react";
|
|
1508
|
+
import { Box as Box5, Text as Text4, useApp as useApp3, useInput as useInput3 } from "ink";
|
|
1284
1509
|
|
|
1285
1510
|
// src/util/shuffle.ts
|
|
1286
1511
|
function shuffle(arr, rng = Math.random) {
|
|
@@ -1479,7 +1704,7 @@ function topN(book, n) {
|
|
|
1479
1704
|
}
|
|
1480
1705
|
|
|
1481
1706
|
// src/ui/hooks/useWordLoop.ts
|
|
1482
|
-
import { useEffect as
|
|
1707
|
+
import { useEffect as useEffect4, useReducer, useRef, useState as useState7 } from "react";
|
|
1483
1708
|
import { useInput as useInput2, useApp as useApp2 } from "ink";
|
|
1484
1709
|
function reducer(state2, action) {
|
|
1485
1710
|
if (action.type === "start") {
|
|
@@ -1513,7 +1738,7 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1513
1738
|
lastEffect: null
|
|
1514
1739
|
}));
|
|
1515
1740
|
const completedRef = useRef(false);
|
|
1516
|
-
const [tick, setTick] =
|
|
1741
|
+
const [tick, setTick] = useState7(0);
|
|
1517
1742
|
const { exit } = useApp2();
|
|
1518
1743
|
useInput2(
|
|
1519
1744
|
(input, key) => {
|
|
@@ -1545,13 +1770,13 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1545
1770
|
},
|
|
1546
1771
|
{ isActive: enabled }
|
|
1547
1772
|
);
|
|
1548
|
-
|
|
1773
|
+
useEffect4(() => {
|
|
1549
1774
|
if (state2.session.finishedAt !== null && !completedRef.current) {
|
|
1550
1775
|
completedRef.current = true;
|
|
1551
1776
|
onComplete(state2.session);
|
|
1552
1777
|
}
|
|
1553
1778
|
}, [state2.session, onComplete]);
|
|
1554
|
-
|
|
1779
|
+
useEffect4(() => {
|
|
1555
1780
|
if (state2.session.finishedAt !== null) return;
|
|
1556
1781
|
const id = setInterval(() => setTick((t) => t + 1), 1e3);
|
|
1557
1782
|
return () => clearInterval(id);
|
|
@@ -1560,10 +1785,10 @@ function useWordLoop({ playlist, onComplete, onTab, onEscape, onSkip, enabled =
|
|
|
1560
1785
|
}
|
|
1561
1786
|
|
|
1562
1787
|
// src/ui/hooks/useAudio.ts
|
|
1563
|
-
import { useEffect as
|
|
1788
|
+
import { useEffect as useEffect5, useRef as useRef2 } from "react";
|
|
1564
1789
|
function useAudio(opts) {
|
|
1565
1790
|
const initedRef = useRef2(false);
|
|
1566
|
-
|
|
1791
|
+
useEffect5(() => {
|
|
1567
1792
|
if (initedRef.current) return;
|
|
1568
1793
|
initedRef.current = true;
|
|
1569
1794
|
initAudio(!opts.enabled).catch(() => void 0);
|
|
@@ -1640,6 +1865,12 @@ function sparkline(values) {
|
|
|
1640
1865
|
return SPARK[Math.max(0, Math.min(SPARK.length - 1, idx))];
|
|
1641
1866
|
}).join("");
|
|
1642
1867
|
}
|
|
1868
|
+
function dailyValues(sessions, days, metric, now = /* @__PURE__ */ new Date()) {
|
|
1869
|
+
const buckets = dailyBuckets(sessions, days, now);
|
|
1870
|
+
if (metric === "wpm") return buckets.map((b) => b.wpm);
|
|
1871
|
+
if (metric === "accuracy") return buckets.map((b) => b.accuracy * 100);
|
|
1872
|
+
return buckets.map((b) => b.sessions);
|
|
1873
|
+
}
|
|
1643
1874
|
function dailyBuckets(sessions, days, now = /* @__PURE__ */ new Date()) {
|
|
1644
1875
|
const out = [];
|
|
1645
1876
|
const byDay = /* @__PURE__ */ new Map();
|
|
@@ -1737,16 +1968,114 @@ function useSessionPersistence(meta) {
|
|
|
1737
1968
|
);
|
|
1738
1969
|
}
|
|
1739
1970
|
|
|
1971
|
+
// src/ui/screens/StealthPracticeLayout.tsx
|
|
1972
|
+
import { Box as Box4, Text as Text3, useStdout as useStdout2 } from "ink";
|
|
1973
|
+
import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1974
|
+
var TYPED = "#d4d4d4";
|
|
1975
|
+
var UNTYPED = "#808080";
|
|
1976
|
+
var DIM = "#6b6b6b";
|
|
1977
|
+
var RIGHT_WIDTH = 28;
|
|
1978
|
+
function fmtTime(ms) {
|
|
1979
|
+
const total = Math.floor(ms / 1e3);
|
|
1980
|
+
const m = Math.floor(total / 60);
|
|
1981
|
+
const s = total % 60;
|
|
1982
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
1983
|
+
}
|
|
1984
|
+
function useLeftWidth() {
|
|
1985
|
+
const { stdout } = useStdout2();
|
|
1986
|
+
const cols = stdout?.columns ?? 80;
|
|
1987
|
+
return Math.max(20, cols - RIGHT_WIDTH);
|
|
1988
|
+
}
|
|
1989
|
+
function Row({ left, right }) {
|
|
1990
|
+
const leftWidth = useLeftWidth();
|
|
1991
|
+
return /* @__PURE__ */ jsxs2(Box4, { children: [
|
|
1992
|
+
/* @__PURE__ */ jsx9(Box4, { width: leftWidth, children: left }),
|
|
1993
|
+
/* @__PURE__ */ jsx9(Box4, { width: RIGHT_WIDTH, justifyContent: "flex-end", children: right })
|
|
1994
|
+
] });
|
|
1995
|
+
}
|
|
1996
|
+
function StealthTyping(props) {
|
|
1997
|
+
const t = useStrings();
|
|
1998
|
+
const target = [...props.target];
|
|
1999
|
+
const typed = [...props.typed];
|
|
2000
|
+
const wordCell = /* @__PURE__ */ jsxs2(Box4, { children: [
|
|
2001
|
+
/* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "[" }),
|
|
2002
|
+
target.map((ch, i) => {
|
|
2003
|
+
const isTyped = i < typed.length;
|
|
2004
|
+
const display = props.hideTarget && !isTyped ? "_" : isTyped ? typed[i] : ch;
|
|
2005
|
+
const color = isTyped ? TYPED : UNTYPED;
|
|
2006
|
+
return /* @__PURE__ */ jsx9(Text3, { color, inverse: props.error && isTyped && i === typed.length - 1, children: display }, i);
|
|
2007
|
+
}),
|
|
2008
|
+
/* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: "]" })
|
|
2009
|
+
] });
|
|
2010
|
+
const phoneticTransCell = /* @__PURE__ */ jsxs2(Box4, { children: [
|
|
2011
|
+
props.phonetic && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.phonetic }),
|
|
2012
|
+
props.phonetic && props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: " \xB7 " }),
|
|
2013
|
+
props.translation.length > 0 && /* @__PURE__ */ jsx9(Text3, { color: DIM, children: props.translation.slice(0, 1).join("") })
|
|
2014
|
+
] });
|
|
2015
|
+
const info = props.info;
|
|
2016
|
+
const accFmt = Number.isInteger(info.accPct) ? `${info.accPct}` : info.accPct.toFixed(1);
|
|
2017
|
+
const right1 = info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: `${info.dictName} \xB7 ${info.chapterLabel}` }) : /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
|
|
2018
|
+
"Ctrl+I ",
|
|
2019
|
+
t.stealth.infoChipLabel
|
|
2020
|
+
] });
|
|
2021
|
+
const right2 = info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: `${info.completed}/${info.total} \xB7 ${info.wpm}wpm \xB7 ${accFmt}%` }) : /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
|
|
2022
|
+
"Esc ",
|
|
2023
|
+
t.common.back
|
|
2024
|
+
] });
|
|
2025
|
+
const right3 = info.visible ? /* @__PURE__ */ jsx9(Text3, { color: DIM, children: fmtTime(info.elapsedMs) }) : /* @__PURE__ */ jsx9(Text3, { children: " " });
|
|
2026
|
+
return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
|
|
2027
|
+
/* @__PURE__ */ jsx9(Row, { left: wordCell, right: right1 }),
|
|
2028
|
+
/* @__PURE__ */ jsx9(Row, { left: phoneticTransCell, right: right2 }),
|
|
2029
|
+
/* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: right3 })
|
|
2030
|
+
] });
|
|
2031
|
+
}
|
|
2032
|
+
function StealthPaused() {
|
|
2033
|
+
const t = useStrings();
|
|
2034
|
+
return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
|
|
2035
|
+
/* @__PURE__ */ jsx9(
|
|
2036
|
+
Row,
|
|
2037
|
+
{
|
|
2038
|
+
left: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: t.stealth.paused }),
|
|
2039
|
+
right: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.pausedHintRight })
|
|
2040
|
+
}
|
|
2041
|
+
),
|
|
2042
|
+
/* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
|
|
2043
|
+
"Esc ",
|
|
2044
|
+
t.common.back
|
|
2045
|
+
] }) }),
|
|
2046
|
+
/* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsx9(Text3, { children: " " }) })
|
|
2047
|
+
] });
|
|
2048
|
+
}
|
|
2049
|
+
function StealthSummary(props) {
|
|
2050
|
+
const t = useStrings();
|
|
2051
|
+
const accFmt = Number.isInteger(props.accPct) ? `${props.accPct}` : props.accPct.toFixed(1);
|
|
2052
|
+
const line = `${t.stealth.chapterDone} \xB7 ${props.wordCount}w \xB7 ${props.wpm}wpm \xB7 ${accFmt}% \xB7 ${fmtTime(props.durationMs)}`;
|
|
2053
|
+
return /* @__PURE__ */ jsxs2(Box4, { flexDirection: "column", children: [
|
|
2054
|
+
/* @__PURE__ */ jsx9(
|
|
2055
|
+
Row,
|
|
2056
|
+
{
|
|
2057
|
+
left: /* @__PURE__ */ jsx9(Text3, { color: UNTYPED, children: line }),
|
|
2058
|
+
right: /* @__PURE__ */ jsx9(Text3, { color: DIM, children: t.stealth.nextHintRight })
|
|
2059
|
+
}
|
|
2060
|
+
),
|
|
2061
|
+
/* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsxs2(Text3, { color: DIM, children: [
|
|
2062
|
+
"Esc ",
|
|
2063
|
+
t.common.back
|
|
2064
|
+
] }) }),
|
|
2065
|
+
/* @__PURE__ */ jsx9(Row, { left: /* @__PURE__ */ jsx9(Text3, { children: " " }), right: /* @__PURE__ */ jsx9(Text3, { children: " " }) })
|
|
2066
|
+
] });
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1740
2069
|
// src/ui/screens/PracticeScreen.tsx
|
|
1741
|
-
import { jsx as
|
|
2070
|
+
import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1742
2071
|
function PracticeScreen({ params }) {
|
|
1743
2072
|
const { dictId, chapterIndex, mode } = params;
|
|
1744
2073
|
const { cfg } = useAppState();
|
|
1745
2074
|
const t = useStrings();
|
|
1746
|
-
const [phase, setPhase] =
|
|
1747
|
-
const [loaded, setLoaded] =
|
|
1748
|
-
const [errorMsg, setErrorMsg] =
|
|
1749
|
-
|
|
2075
|
+
const [phase, setPhase] = useState8("loading");
|
|
2076
|
+
const [loaded, setLoaded] = useState8(null);
|
|
2077
|
+
const [errorMsg, setErrorMsg] = useState8(null);
|
|
2078
|
+
useEffect6(() => {
|
|
1750
2079
|
let cancelled = false;
|
|
1751
2080
|
setPhase("loading");
|
|
1752
2081
|
setLoaded(null);
|
|
@@ -1789,13 +2118,13 @@ function PracticeScreen({ params }) {
|
|
|
1789
2118
|
};
|
|
1790
2119
|
}, [dictId, chapterIndex, mode, cfg.chapterSize, t]);
|
|
1791
2120
|
if (phase === "loading") {
|
|
1792
|
-
return /* @__PURE__ */
|
|
2121
|
+
return /* @__PURE__ */ jsx10(Centered, { text: t.practice.loading, color: PALETTE.muted });
|
|
1793
2122
|
}
|
|
1794
2123
|
if (phase === "error") {
|
|
1795
|
-
return /* @__PURE__ */
|
|
2124
|
+
return /* @__PURE__ */ jsx10(ErrorView, { msg: errorMsg ?? t.practice.errors.unknown });
|
|
1796
2125
|
}
|
|
1797
2126
|
if (!loaded) return null;
|
|
1798
|
-
return /* @__PURE__ */
|
|
2127
|
+
return /* @__PURE__ */ jsx10(
|
|
1799
2128
|
PracticeRunner,
|
|
1800
2129
|
{
|
|
1801
2130
|
params,
|
|
@@ -1803,7 +2132,7 @@ function PracticeScreen({ params }) {
|
|
|
1803
2132
|
phase,
|
|
1804
2133
|
setPhase
|
|
1805
2134
|
},
|
|
1806
|
-
`${dictId}-${chapterIndex}-${mode}`
|
|
2135
|
+
`${dictId}-${chapterIndex}-${mode}-${params.stealth ? "s" : "n"}`
|
|
1807
2136
|
);
|
|
1808
2137
|
}
|
|
1809
2138
|
function PracticeRunner({
|
|
@@ -1813,19 +2142,28 @@ function PracticeRunner({
|
|
|
1813
2142
|
setPhase
|
|
1814
2143
|
}) {
|
|
1815
2144
|
const { dictId, chapterIndex, mode } = params;
|
|
2145
|
+
const stealth = params.stealth === true;
|
|
1816
2146
|
const { cfg } = useAppState();
|
|
1817
2147
|
const nav = useNav();
|
|
1818
2148
|
const { exit } = useApp3();
|
|
1819
2149
|
const goBack = () => nav.stack.length > 1 ? nav.back() : exit();
|
|
1820
2150
|
const persist = useSessionPersistence({ dictId, chapterIndex, mode });
|
|
2151
|
+
const dictName = useDictName(dictId);
|
|
1821
2152
|
const audio = useAudio({
|
|
1822
|
-
enabled: cfg.sounds.master,
|
|
2153
|
+
enabled: !stealth && cfg.sounds.master,
|
|
1823
2154
|
accent: cfg.accent,
|
|
1824
|
-
autoplayPronunciation: cfg.autoplayPronunciation
|
|
2155
|
+
autoplayPronunciation: !stealth && cfg.autoplayPronunciation
|
|
1825
2156
|
});
|
|
1826
2157
|
const finishedRef = useRef3(false);
|
|
1827
2158
|
const lastEffectRef = useRef3(null);
|
|
1828
2159
|
const lastIndexRef = useRef3(-1);
|
|
2160
|
+
const [infoVisible, setInfoVisible] = useState8(false);
|
|
2161
|
+
const [infoShownAt, setInfoShownAt] = useState8(null);
|
|
2162
|
+
useEffect6(() => {
|
|
2163
|
+
if (infoShownAt === null) return;
|
|
2164
|
+
const id = setTimeout(() => setInfoVisible(false), 2e3);
|
|
2165
|
+
return () => clearTimeout(id);
|
|
2166
|
+
}, [infoShownAt]);
|
|
1829
2167
|
const { session, lastEffect, tick } = useWordLoop({
|
|
1830
2168
|
playlist: loaded.playlist,
|
|
1831
2169
|
enabled: phase === "typing",
|
|
@@ -1838,12 +2176,13 @@ function PracticeRunner({
|
|
|
1838
2176
|
});
|
|
1839
2177
|
},
|
|
1840
2178
|
onEscape: () => setPhase(phase === "paused" ? "typing" : "paused"),
|
|
1841
|
-
onTab: () => {
|
|
2179
|
+
onTab: stealth ? void 0 : () => {
|
|
1842
2180
|
const cur = session.current ? loaded.playlist[session.current.wordIndex] : void 0;
|
|
1843
2181
|
if (cur) void audio.pronounce(cur.name);
|
|
1844
2182
|
}
|
|
1845
2183
|
});
|
|
1846
|
-
|
|
2184
|
+
useEffect6(() => {
|
|
2185
|
+
if (stealth) return;
|
|
1847
2186
|
if (lastEffect === null) return;
|
|
1848
2187
|
if (lastEffect === lastEffectRef.current) return;
|
|
1849
2188
|
lastEffectRef.current = lastEffect;
|
|
@@ -1853,8 +2192,9 @@ function PracticeRunner({
|
|
|
1853
2192
|
if (cfg.sounds.feedback) audio.correct();
|
|
1854
2193
|
if (cfg.sounds.keystroke) audio.keystroke();
|
|
1855
2194
|
}
|
|
1856
|
-
}, [lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
|
|
1857
|
-
|
|
2195
|
+
}, [stealth, lastEffect, audio, cfg.sounds.feedback, cfg.sounds.keystroke]);
|
|
2196
|
+
useEffect6(() => {
|
|
2197
|
+
if (stealth) return;
|
|
1858
2198
|
const idx = session.current?.wordIndex ?? -1;
|
|
1859
2199
|
if (idx === -1) return;
|
|
1860
2200
|
if (idx === lastIndexRef.current) return;
|
|
@@ -1863,63 +2203,150 @@ function PracticeRunner({
|
|
|
1863
2203
|
const next = loaded.playlist[idx + 1];
|
|
1864
2204
|
if (cur && cfg.autoplayPronunciation) audio.pronounce(cur.name);
|
|
1865
2205
|
if (next) audio.prefetch(next.name);
|
|
1866
|
-
}, [session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
|
|
2206
|
+
}, [stealth, session.current?.wordIndex, audio, cfg.autoplayPronunciation, loaded.playlist]);
|
|
1867
2207
|
void tick;
|
|
1868
2208
|
useInput3(
|
|
1869
|
-
(input) => {
|
|
1870
|
-
if (input === "
|
|
1871
|
-
|
|
2209
|
+
(input, key) => {
|
|
2210
|
+
if (key.ctrl && input === "i") {
|
|
2211
|
+
setInfoVisible(true);
|
|
2212
|
+
setInfoShownAt(Date.now());
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
},
|
|
2216
|
+
{ isActive: stealth && phase === "typing" }
|
|
2217
|
+
);
|
|
2218
|
+
useInput3(
|
|
2219
|
+
(_input, key) => {
|
|
2220
|
+
if (key.return) {
|
|
2221
|
+
setPhase("typing");
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
if (key.escape) {
|
|
2225
|
+
goBack();
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
1872
2228
|
},
|
|
1873
2229
|
{ isActive: phase === "paused" }
|
|
1874
2230
|
);
|
|
1875
2231
|
useInput3(
|
|
1876
|
-
(input) => {
|
|
1877
|
-
if (
|
|
2232
|
+
(input, key) => {
|
|
2233
|
+
if (key.escape) {
|
|
1878
2234
|
goBack();
|
|
1879
2235
|
return;
|
|
1880
2236
|
}
|
|
1881
|
-
if (
|
|
2237
|
+
if (key.return) {
|
|
1882
2238
|
const nextIdx = chapterIndex + 1;
|
|
1883
2239
|
if (mode === "loop") {
|
|
1884
|
-
nav.replace({
|
|
2240
|
+
nav.replace({
|
|
2241
|
+
name: "practice",
|
|
2242
|
+
params: { dictId, chapterIndex, mode, stealth: params.stealth }
|
|
2243
|
+
});
|
|
1885
2244
|
} else if (mode === "review" || nextIdx >= loaded.totalChapters) {
|
|
1886
2245
|
goBack();
|
|
1887
2246
|
} else {
|
|
1888
|
-
nav.replace({
|
|
2247
|
+
nav.replace({
|
|
2248
|
+
name: "practice",
|
|
2249
|
+
params: { dictId, chapterIndex: nextIdx, mode, stealth: params.stealth }
|
|
2250
|
+
});
|
|
1889
2251
|
}
|
|
1890
2252
|
return;
|
|
1891
2253
|
}
|
|
1892
2254
|
if (input === "m") {
|
|
1893
|
-
nav.replace({
|
|
2255
|
+
nav.replace({
|
|
2256
|
+
name: "practice",
|
|
2257
|
+
params: { dictId, chapterIndex: 0, mode: "review", stealth: params.stealth }
|
|
2258
|
+
});
|
|
1894
2259
|
return;
|
|
1895
2260
|
}
|
|
1896
2261
|
},
|
|
1897
2262
|
{ isActive: phase === "summary" }
|
|
1898
2263
|
);
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2264
|
+
const completed = session.results.length;
|
|
2265
|
+
const errors = session.results.reduce((a, r) => a + r.errors, 0);
|
|
2266
|
+
const elapsedMs = Date.now() - session.startedAt;
|
|
2267
|
+
const minutes = elapsedMs / 6e4;
|
|
2268
|
+
const wpm = minutes > 0 ? Math.round(completed / minutes * 10) / 10 : 0;
|
|
2269
|
+
const summary = phase === "summary" ? sessionSummary(session) : null;
|
|
2270
|
+
if (stealth) {
|
|
2271
|
+
if (phase === "paused") return /* @__PURE__ */ jsx10(StealthPaused, {});
|
|
2272
|
+
if (phase === "summary" && summary) {
|
|
2273
|
+
const sMinutes = summary.durationMs / 6e4;
|
|
2274
|
+
const sWpm = sMinutes > 0 ? Math.round(summary.wordCount / sMinutes * 10) / 10 : 0;
|
|
2275
|
+
const sErrWords = Object.keys(summary.perWordErrors).length;
|
|
2276
|
+
const sAcc = summary.wordCount === 0 ? 1 : Math.max(0, (summary.wordCount - sErrWords) / summary.wordCount);
|
|
2277
|
+
const sAccPct = Math.round(sAcc * 1e3) / 10;
|
|
2278
|
+
return /* @__PURE__ */ jsx10(
|
|
2279
|
+
StealthSummary,
|
|
2280
|
+
{
|
|
2281
|
+
wordCount: summary.wordCount,
|
|
2282
|
+
errors: summary.errors,
|
|
2283
|
+
durationMs: summary.durationMs,
|
|
2284
|
+
wpm: sWpm,
|
|
2285
|
+
accPct: sAccPct
|
|
2286
|
+
}
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
const currentWord2 = session.current ? loaded.playlist[session.current.wordIndex] : loaded.playlist[loaded.playlist.length - 1];
|
|
2290
|
+
const inputState2 = session.current?.input ?? { target: "", typed: "", errorsThisWord: 0 };
|
|
2291
|
+
const errWords = new Set(
|
|
2292
|
+
session.results.filter((r) => r.errors > 0).map((r) => r.word)
|
|
2293
|
+
).size;
|
|
2294
|
+
const accFrac = completed === 0 ? 1 : Math.max(0, (completed - errWords) / completed);
|
|
2295
|
+
const accPct = Math.round(accFrac * 1e3) / 10;
|
|
2296
|
+
const chapterLabel = mode === "review" ? "review" : `ch ${chapterIndex + 1}/${loaded.totalChapters}`;
|
|
2297
|
+
return /* @__PURE__ */ jsx10(
|
|
2298
|
+
StealthTyping,
|
|
2299
|
+
{
|
|
2300
|
+
target: currentWord2?.name ?? "",
|
|
2301
|
+
typed: inputState2.typed,
|
|
2302
|
+
hideTarget: mode === "dictation",
|
|
2303
|
+
phonetic: pickPhonetic(currentWord2, cfg.accent),
|
|
2304
|
+
translation: currentWord2?.trans ?? [],
|
|
2305
|
+
error: lastEffect === "wrong",
|
|
2306
|
+
info: {
|
|
2307
|
+
visible: infoVisible,
|
|
2308
|
+
dictName: truncateName(dictName, 24),
|
|
2309
|
+
chapterLabel,
|
|
2310
|
+
completed,
|
|
2311
|
+
total: loaded.playlist.length,
|
|
2312
|
+
wpm,
|
|
2313
|
+
accPct,
|
|
2314
|
+
elapsedMs
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
if (phase === "paused") {
|
|
2320
|
+
return /* @__PURE__ */ jsx10(
|
|
2321
|
+
PausedView,
|
|
2322
|
+
{
|
|
2323
|
+
dictName,
|
|
2324
|
+
chapterIndex,
|
|
2325
|
+
totalChapters: loaded.totalChapters,
|
|
2326
|
+
mode,
|
|
2327
|
+
completed,
|
|
2328
|
+
total: loaded.playlist.length
|
|
2329
|
+
}
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
if (phase === "summary" && summary) {
|
|
2333
|
+
return /* @__PURE__ */ jsx10(
|
|
1902
2334
|
SummaryView,
|
|
1903
2335
|
{
|
|
1904
|
-
|
|
2336
|
+
dictName,
|
|
1905
2337
|
chapterIndex,
|
|
1906
2338
|
totalChapters: loaded.totalChapters,
|
|
1907
2339
|
mode,
|
|
1908
|
-
summary
|
|
2340
|
+
summary
|
|
1909
2341
|
}
|
|
1910
2342
|
);
|
|
1911
2343
|
}
|
|
1912
2344
|
const currentWord = session.current ? loaded.playlist[session.current.wordIndex] : loaded.playlist[loaded.playlist.length - 1];
|
|
1913
2345
|
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(
|
|
2346
|
+
return /* @__PURE__ */ jsx10(
|
|
1920
2347
|
TypingLayout,
|
|
1921
2348
|
{
|
|
1922
|
-
|
|
2349
|
+
dictName,
|
|
1923
2350
|
chapterIndex,
|
|
1924
2351
|
totalChapters: loaded.totalChapters,
|
|
1925
2352
|
mode,
|
|
@@ -1943,7 +2370,7 @@ function pickPhonetic(word, accent) {
|
|
|
1943
2370
|
const p = accent === "us" ? word.usphone : word.ukphone;
|
|
1944
2371
|
return p ? `/${p}/` : null;
|
|
1945
2372
|
}
|
|
1946
|
-
function
|
|
2373
|
+
function fmtTime2(ms) {
|
|
1947
2374
|
const total = Math.floor(ms / 1e3);
|
|
1948
2375
|
const m = Math.floor(total / 60);
|
|
1949
2376
|
const s = total % 60;
|
|
@@ -1952,11 +2379,11 @@ function fmtTime(ms) {
|
|
|
1952
2379
|
function TypingLayout(props) {
|
|
1953
2380
|
const t = useStrings();
|
|
1954
2381
|
const progressFrac = props.total === 0 ? 0 : props.completed / props.total;
|
|
1955
|
-
return /* @__PURE__ */ jsxs3(
|
|
1956
|
-
/* @__PURE__ */
|
|
2382
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2383
|
+
/* @__PURE__ */ jsx10(
|
|
1957
2384
|
StatusBar,
|
|
1958
2385
|
{
|
|
1959
|
-
|
|
2386
|
+
dictName: props.dictName,
|
|
1960
2387
|
chapterIndex: props.chapterIndex,
|
|
1961
2388
|
totalChapters: props.totalChapters,
|
|
1962
2389
|
mode: props.mode,
|
|
@@ -1966,8 +2393,8 @@ function TypingLayout(props) {
|
|
|
1966
2393
|
elapsedMs: props.elapsedMs
|
|
1967
2394
|
}
|
|
1968
2395
|
),
|
|
1969
|
-
/* @__PURE__ */ jsxs3(
|
|
1970
|
-
/* @__PURE__ */
|
|
2396
|
+
/* @__PURE__ */ jsxs3(Box5, { flexGrow: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", children: [
|
|
2397
|
+
/* @__PURE__ */ jsx10(
|
|
1971
2398
|
BigWord,
|
|
1972
2399
|
{
|
|
1973
2400
|
target: props.target,
|
|
@@ -1976,17 +2403,17 @@ function TypingLayout(props) {
|
|
|
1976
2403
|
hideTarget: props.hideTarget
|
|
1977
2404
|
}
|
|
1978
2405
|
),
|
|
1979
|
-
props.phonetic && /* @__PURE__ */
|
|
1980
|
-
props.translation.length > 0 && /* @__PURE__ */
|
|
2406
|
+
props.phonetic && /* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { italic: true, color: PALETTE.muted, children: props.phonetic }) }),
|
|
2407
|
+
props.translation.length > 0 && /* @__PURE__ */ jsx10(Box5, { marginTop: 1, flexDirection: "column", alignItems: "center", children: props.translation.slice(0, 2).map((tr, i) => /* @__PURE__ */ jsx10(Text4, { color: PALETTE.primary, children: tr }, i)) })
|
|
1981
2408
|
] }),
|
|
1982
|
-
/* @__PURE__ */ jsxs3(
|
|
1983
|
-
/* @__PURE__ */
|
|
1984
|
-
/* @__PURE__ */
|
|
2409
|
+
/* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", children: [
|
|
2410
|
+
/* @__PURE__ */ jsx10(ProgressBar, { frac: progressFrac }),
|
|
2411
|
+
/* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsxs3(Text4, { color: PALETTE.muted, children: [
|
|
1985
2412
|
props.completed,
|
|
1986
2413
|
"/",
|
|
1987
2414
|
props.total,
|
|
1988
2415
|
" \xB7 ",
|
|
1989
|
-
|
|
2416
|
+
fmtTime2(props.elapsedMs),
|
|
1990
2417
|
" \xB7 ",
|
|
1991
2418
|
props.wpm,
|
|
1992
2419
|
" ",
|
|
@@ -1996,7 +2423,7 @@ function TypingLayout(props) {
|
|
|
1996
2423
|
" ",
|
|
1997
2424
|
t.practice.statCards.errors
|
|
1998
2425
|
] }) }),
|
|
1999
|
-
/* @__PURE__ */
|
|
2426
|
+
/* @__PURE__ */ jsx10(Box5, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.footers.typing }) })
|
|
2000
2427
|
] })
|
|
2001
2428
|
] });
|
|
2002
2429
|
}
|
|
@@ -2004,12 +2431,13 @@ function StatusBar(props) {
|
|
|
2004
2431
|
const t = useStrings();
|
|
2005
2432
|
const modeName = t.practice.modes[props.mode];
|
|
2006
2433
|
const accentName = t.practice.accents[props.accent];
|
|
2007
|
-
const
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
/* @__PURE__ */
|
|
2012
|
-
/* @__PURE__ */
|
|
2434
|
+
const name = truncateName(props.dictName, 20);
|
|
2435
|
+
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}`;
|
|
2436
|
+
const right = `${props.completed}/${props.total} \xB7 ${fmtTime2(props.elapsedMs)}`;
|
|
2437
|
+
return /* @__PURE__ */ jsxs3(Box5, { children: [
|
|
2438
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: left }),
|
|
2439
|
+
/* @__PURE__ */ jsx10(Box5, { flexGrow: 1 }),
|
|
2440
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: right })
|
|
2013
2441
|
] });
|
|
2014
2442
|
}
|
|
2015
2443
|
function ProgressBar({ frac }) {
|
|
@@ -2017,38 +2445,43 @@ function ProgressBar({ frac }) {
|
|
|
2017
2445
|
const width = Math.max(20, Math.min(72, cols - 16));
|
|
2018
2446
|
const filled = Math.round(width * Math.max(0, Math.min(1, frac)));
|
|
2019
2447
|
const empty = width - filled;
|
|
2020
|
-
return /* @__PURE__ */ jsxs3(
|
|
2021
|
-
/* @__PURE__ */
|
|
2022
|
-
/* @__PURE__ */
|
|
2448
|
+
return /* @__PURE__ */ jsxs3(Box5, { justifyContent: "center", children: [
|
|
2449
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.accent, children: "\u2501".repeat(filled) }),
|
|
2450
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: "\u2500".repeat(empty) })
|
|
2023
2451
|
] });
|
|
2024
2452
|
}
|
|
2025
|
-
function PausedView() {
|
|
2453
|
+
function PausedView(props) {
|
|
2026
2454
|
const t = useStrings();
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2455
|
+
const frac = props.total === 0 ? 0 : props.completed / props.total;
|
|
2456
|
+
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)}`;
|
|
2457
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2458
|
+
/* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.warning, children: t.practice.pause.title }),
|
|
2459
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: subtitle }) }),
|
|
2460
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(ProgressBar, { frac }) }),
|
|
2461
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.pause.progress(props.completed, props.total) }) }),
|
|
2462
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.pause.hint }) })
|
|
2030
2463
|
] });
|
|
2031
2464
|
}
|
|
2032
2465
|
function ErrorView({ msg }) {
|
|
2033
2466
|
const t = useStrings();
|
|
2034
|
-
return /* @__PURE__ */ jsxs3(
|
|
2035
|
-
/* @__PURE__ */
|
|
2036
|
-
/* @__PURE__ */
|
|
2037
|
-
"
|
|
2467
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
2468
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.error, children: msg }),
|
|
2469
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsxs3(Text4, { color: PALETTE.muted, children: [
|
|
2470
|
+
"Esc ",
|
|
2038
2471
|
t.common.back
|
|
2039
2472
|
] }) }),
|
|
2040
|
-
/* @__PURE__ */
|
|
2473
|
+
/* @__PURE__ */ jsx10(BackKey, {})
|
|
2041
2474
|
] });
|
|
2042
2475
|
}
|
|
2043
2476
|
function BackKey() {
|
|
2044
2477
|
const nav = useNav();
|
|
2045
|
-
useInput3((
|
|
2046
|
-
if (
|
|
2478
|
+
useInput3((_input, key) => {
|
|
2479
|
+
if (key.escape) nav.back();
|
|
2047
2480
|
});
|
|
2048
2481
|
return null;
|
|
2049
2482
|
}
|
|
2050
2483
|
function Centered({ text, color }) {
|
|
2051
|
-
return /* @__PURE__ */
|
|
2484
|
+
return /* @__PURE__ */ jsx10(Box5, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx10(Text4, { color, children: text }) });
|
|
2052
2485
|
}
|
|
2053
2486
|
function SummaryView(props) {
|
|
2054
2487
|
const { summary } = props;
|
|
@@ -2059,13 +2492,16 @@ function SummaryView(props) {
|
|
|
2059
2492
|
const accPct = Math.round(acc * 1e3) / 10;
|
|
2060
2493
|
const t = useStrings();
|
|
2061
2494
|
const modeName = t.practice.modes[props.mode];
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2495
|
+
const name = truncateName(props.dictName, 20);
|
|
2496
|
+
const subtitle = props.mode === "review" ? `${name} \xB7 ${t.practice.reviewLabel}` : `${name} \xB7 ${t.practice.chapterLabel(props.chapterIndex + 1, props.totalChapters)} \xB7 ${modeName}`;
|
|
2497
|
+
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;
|
|
2498
|
+
const footer = `Enter ${nextLabel} \xB7 m ${t.practice.summary.reviewMistakes} \xB7 Esc ${t.practice.summary.backMenu}`;
|
|
2499
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 1, width: "100%", height: "100%", children: [
|
|
2500
|
+
/* @__PURE__ */ jsx10(Text4, { bold: true, color: PALETTE.success, children: t.practice.chapterComplete }),
|
|
2501
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: subtitle }) }),
|
|
2502
|
+
/* @__PURE__ */ jsxs3(Box5, { marginTop: 3, flexDirection: "row", justifyContent: "center", children: [
|
|
2503
|
+
/* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.words, value: String(summary.wordCount), color: PALETTE.text }),
|
|
2504
|
+
/* @__PURE__ */ jsx10(
|
|
2069
2505
|
StatCard,
|
|
2070
2506
|
{
|
|
2071
2507
|
label: t.practice.statCards.errors,
|
|
@@ -2073,36 +2509,107 @@ function SummaryView(props) {
|
|
|
2073
2509
|
color: summary.errors > 0 ? PALETTE.error : PALETTE.muted
|
|
2074
2510
|
}
|
|
2075
2511
|
),
|
|
2076
|
-
/* @__PURE__ */
|
|
2077
|
-
/* @__PURE__ */
|
|
2512
|
+
/* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.wpm, value: String(wpm), color: PALETTE.accent }),
|
|
2513
|
+
/* @__PURE__ */ jsx10(StatCard, { label: t.practice.statCards.accuracy, value: `${accPct}%`, color: PALETTE.accent })
|
|
2078
2514
|
] }),
|
|
2079
|
-
/* @__PURE__ */
|
|
2080
|
-
/* @__PURE__ */
|
|
2081
|
-
/* @__PURE__ */
|
|
2515
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: t.practice.statCards.elapsed(fmtTime2(summary.durationMs)) }) }),
|
|
2516
|
+
/* @__PURE__ */ jsx10(Box5, { flexGrow: 1 }),
|
|
2517
|
+
/* @__PURE__ */ jsx10(Box5, { marginTop: 2, children: /* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: footer }) })
|
|
2082
2518
|
] });
|
|
2083
2519
|
}
|
|
2084
2520
|
function StatCard({ label, value, color }) {
|
|
2085
|
-
return /* @__PURE__ */ jsxs3(
|
|
2086
|
-
/* @__PURE__ */
|
|
2087
|
-
/* @__PURE__ */
|
|
2521
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", alignItems: "center", marginX: 3, children: [
|
|
2522
|
+
/* @__PURE__ */ jsx10(Text4, { bold: true, color, children: value }),
|
|
2523
|
+
/* @__PURE__ */ jsx10(Text4, { color: PALETTE.muted, children: label })
|
|
2088
2524
|
] });
|
|
2089
2525
|
}
|
|
2090
2526
|
|
|
2091
2527
|
// src/ui/screens/DictBrowser.tsx
|
|
2092
|
-
import { useEffect as
|
|
2093
|
-
import { Box as
|
|
2094
|
-
|
|
2528
|
+
import { useEffect as useEffect7, useMemo as useMemo2, useState as useState10 } from "react";
|
|
2529
|
+
import { Box as Box7, Text as Text6, useInput as useInput5, useStdout as useStdout3 } from "ink";
|
|
2530
|
+
|
|
2531
|
+
// src/ui/components/ActionPanel.tsx
|
|
2532
|
+
import { useState as useState9 } from "react";
|
|
2533
|
+
import { Box as Box6, Text as Text5, useInput as useInput4 } from "ink";
|
|
2534
|
+
import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2535
|
+
function ActionPanel({ title, items, onClose }) {
|
|
2536
|
+
const enabledIndices = items.map((it, i) => it.disabled ? -1 : i).filter((i) => i >= 0);
|
|
2537
|
+
const initial = enabledIndices[0] ?? 0;
|
|
2538
|
+
const [selected, setSelected] = useState9(initial);
|
|
2539
|
+
useInput4((input, key) => {
|
|
2540
|
+
if (key.escape) {
|
|
2541
|
+
onClose();
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (key.upArrow) {
|
|
2545
|
+
const cur = enabledIndices.indexOf(selected);
|
|
2546
|
+
const next = enabledIndices[(cur - 1 + enabledIndices.length) % enabledIndices.length];
|
|
2547
|
+
if (next !== void 0) setSelected(next);
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
if (key.downArrow) {
|
|
2551
|
+
const cur = enabledIndices.indexOf(selected);
|
|
2552
|
+
const next = enabledIndices[(cur + 1) % enabledIndices.length];
|
|
2553
|
+
if (next !== void 0) setSelected(next);
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (key.return) {
|
|
2557
|
+
const item = items[selected];
|
|
2558
|
+
if (item && !item.disabled) {
|
|
2559
|
+
void item.run();
|
|
2560
|
+
}
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
for (let i = 0; i < items.length; i++) {
|
|
2564
|
+
const it = items[i];
|
|
2565
|
+
if (it.disabled) continue;
|
|
2566
|
+
if (it.key && input === it.key) {
|
|
2567
|
+
void it.run();
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
const maxLabel = Math.max(...items.map((it) => it.label.length));
|
|
2573
|
+
const width = Math.max(maxLabel + 8, title.length + 4, 24);
|
|
2574
|
+
return /* @__PURE__ */ jsxs4(
|
|
2575
|
+
Box6,
|
|
2576
|
+
{
|
|
2577
|
+
flexDirection: "column",
|
|
2578
|
+
borderStyle: "round",
|
|
2579
|
+
borderColor: PALETTE.accent,
|
|
2580
|
+
paddingX: 2,
|
|
2581
|
+
paddingY: 1,
|
|
2582
|
+
width,
|
|
2583
|
+
children: [
|
|
2584
|
+
/* @__PURE__ */ jsx11(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx11(Text5, { bold: true, color: PALETTE.accent, children: title }) }),
|
|
2585
|
+
items.map((it, i) => {
|
|
2586
|
+
const active = i === selected;
|
|
2587
|
+
const color = it.disabled ? PALETTE.muted : active ? PALETTE.text : PALETTE.muted;
|
|
2588
|
+
return /* @__PURE__ */ jsxs4(Box6, { children: [
|
|
2589
|
+
/* @__PURE__ */ jsx11(Text5, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2590
|
+
/* @__PURE__ */ jsx11(Text5, { bold: active, color, children: it.label })
|
|
2591
|
+
] }, i);
|
|
2592
|
+
}),
|
|
2593
|
+
/* @__PURE__ */ jsx11(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text5, { color: PALETTE.muted, children: "\u2191/\u2193 \xB7 Enter \xB7 Esc" }) })
|
|
2594
|
+
]
|
|
2595
|
+
}
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// src/ui/screens/DictBrowser.tsx
|
|
2600
|
+
import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2095
2601
|
function DictBrowser({ params }) {
|
|
2096
2602
|
const nav = useNav();
|
|
2097
2603
|
const { cfg, setCfg } = useAppState();
|
|
2098
2604
|
const t = useStrings();
|
|
2099
|
-
const
|
|
2100
|
-
const [
|
|
2101
|
-
const [
|
|
2102
|
-
const [
|
|
2103
|
-
const [
|
|
2104
|
-
const [pending, setPending] =
|
|
2105
|
-
const [tick, setTick] =
|
|
2605
|
+
const { stdout } = useStdout3();
|
|
2606
|
+
const [rows, setRows] = useState10([]);
|
|
2607
|
+
const [loading, setLoading] = useState10(true);
|
|
2608
|
+
const [selected, setSelected] = useState10(0);
|
|
2609
|
+
const [filter, setFilter] = useState10("");
|
|
2610
|
+
const [pending, setPending] = useState10(null);
|
|
2611
|
+
const [tick, setTick] = useState10(0);
|
|
2612
|
+
const [panel, setPanel] = useState10(null);
|
|
2106
2613
|
const refresh = async () => {
|
|
2107
2614
|
const reg = await loadRegistry();
|
|
2108
2615
|
const flagged = await Promise.all(
|
|
@@ -2111,141 +2618,221 @@ function DictBrowser({ params }) {
|
|
|
2111
2618
|
setRows(flagged);
|
|
2112
2619
|
setLoading(false);
|
|
2113
2620
|
};
|
|
2114
|
-
|
|
2621
|
+
useEffect7(() => {
|
|
2115
2622
|
void refresh();
|
|
2116
2623
|
}, [tick]);
|
|
2117
|
-
const filtered =
|
|
2624
|
+
const filtered = useMemo2(
|
|
2625
|
+
() => filter ? rows.filter((r) => filterRegistry([r.entry], filter).length > 0) : rows,
|
|
2626
|
+
[filter, rows]
|
|
2627
|
+
);
|
|
2118
2628
|
const safeSelected = Math.max(0, Math.min(filtered.length - 1, selected));
|
|
2119
2629
|
const current = filtered[safeSelected];
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2630
|
+
const rowsTotal = stdout?.rows ?? 24;
|
|
2631
|
+
const visibleH = Math.max(6, rowsTotal - 8);
|
|
2632
|
+
const half = Math.floor(visibleH / 2);
|
|
2633
|
+
const start2 = Math.max(0, Math.min(filtered.length - visibleH, safeSelected - half));
|
|
2634
|
+
const end = Math.min(filtered.length, start2 + visibleH);
|
|
2635
|
+
const goPractice = (id) => {
|
|
2636
|
+
nav.replace({
|
|
2637
|
+
name: "practice",
|
|
2638
|
+
params: {
|
|
2639
|
+
dictId: id,
|
|
2640
|
+
chapterIndex: 0,
|
|
2641
|
+
mode: cfg.defaultMode,
|
|
2642
|
+
stealth: cfg.stealth === "default"
|
|
2125
2643
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2644
|
+
});
|
|
2645
|
+
};
|
|
2646
|
+
const doSetDefault = async (id, navigate = true) => {
|
|
2647
|
+
await setCfg({ ...cfg, defaultDict: id });
|
|
2648
|
+
setPanel(null);
|
|
2649
|
+
if (navigate) {
|
|
2650
|
+
if (params?.pickerMode === "choose-then-practice") {
|
|
2651
|
+
goPractice(id);
|
|
2652
|
+
} else {
|
|
2653
|
+
nav.back();
|
|
2129
2654
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
const doDelete = (id) => {
|
|
2658
|
+
setPanel(null);
|
|
2659
|
+
setPending({ kind: "removing", id });
|
|
2660
|
+
void (async () => {
|
|
2661
|
+
try {
|
|
2662
|
+
await removeDictionary(id);
|
|
2663
|
+
setPending(null);
|
|
2664
|
+
setTick((n) => n + 1);
|
|
2665
|
+
} catch (err) {
|
|
2666
|
+
setPending({ kind: "error", id, msg: err.message });
|
|
2667
|
+
}
|
|
2668
|
+
})();
|
|
2669
|
+
};
|
|
2670
|
+
const doPull = (id) => {
|
|
2671
|
+
setPanel(null);
|
|
2672
|
+
setPending({ kind: "pulling", id });
|
|
2673
|
+
void (async () => {
|
|
2674
|
+
try {
|
|
2675
|
+
await pullDictionary(id);
|
|
2676
|
+
setPending(null);
|
|
2677
|
+
setTick((n) => n + 1);
|
|
2678
|
+
} catch (err) {
|
|
2679
|
+
setPending({ kind: "error", id, msg: err.message });
|
|
2132
2680
|
}
|
|
2681
|
+
})();
|
|
2682
|
+
};
|
|
2683
|
+
const doRefreshList = () => {
|
|
2684
|
+
setPanel(null);
|
|
2685
|
+
setPending({ kind: "refreshing" });
|
|
2686
|
+
setTick((n) => n + 1);
|
|
2687
|
+
setPending(null);
|
|
2688
|
+
};
|
|
2689
|
+
useInput5((input, key) => {
|
|
2690
|
+
if (panel !== null) return;
|
|
2691
|
+
if (key.escape) {
|
|
2692
|
+
nav.back();
|
|
2133
2693
|
return;
|
|
2134
2694
|
}
|
|
2135
|
-
if (key.upArrow)
|
|
2136
|
-
|
|
2137
|
-
if (input === "/") {
|
|
2138
|
-
setFilterFocus(true);
|
|
2695
|
+
if (key.upArrow) {
|
|
2696
|
+
setSelected((i) => Math.max(0, i - 1));
|
|
2139
2697
|
return;
|
|
2140
2698
|
}
|
|
2141
|
-
if (key.
|
|
2142
|
-
|
|
2699
|
+
if (key.downArrow) {
|
|
2700
|
+
setSelected((i) => Math.min(filtered.length - 1, i + 1));
|
|
2143
2701
|
return;
|
|
2144
2702
|
}
|
|
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
|
-
})();
|
|
2703
|
+
if (key.ctrl && input === "k") {
|
|
2704
|
+
setPanel("more");
|
|
2158
2705
|
return;
|
|
2159
2706
|
}
|
|
2160
|
-
if (
|
|
2161
|
-
|
|
2162
|
-
name: "practice",
|
|
2163
|
-
params: { dictId: current.entry.id, chapterIndex: 0, mode: cfg.defaultMode }
|
|
2164
|
-
});
|
|
2707
|
+
if (key.return) {
|
|
2708
|
+
if (current) setPanel("item");
|
|
2165
2709
|
return;
|
|
2166
2710
|
}
|
|
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
|
-
})();
|
|
2711
|
+
if (key.backspace || key.delete) {
|
|
2712
|
+
setFilter((f) => f.slice(0, -1));
|
|
2713
|
+
setSelected(0);
|
|
2178
2714
|
return;
|
|
2179
2715
|
}
|
|
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
|
-
})();
|
|
2716
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
2717
|
+
setFilter((f) => f + input);
|
|
2718
|
+
setSelected(0);
|
|
2191
2719
|
}
|
|
2192
2720
|
});
|
|
2193
2721
|
if (loading) {
|
|
2194
|
-
return /* @__PURE__ */
|
|
2722
|
+
return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.loading }) });
|
|
2195
2723
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2724
|
+
const itemPanelItems = current ? [
|
|
2725
|
+
{
|
|
2726
|
+
label: t.dict.action.setDefault,
|
|
2727
|
+
run: () => void doSetDefault(current.entry.id, params?.pickerMode !== void 0)
|
|
2728
|
+
},
|
|
2729
|
+
{
|
|
2730
|
+
label: t.dict.action.practice,
|
|
2731
|
+
run: () => goPractice(current.entry.id)
|
|
2732
|
+
},
|
|
2733
|
+
{
|
|
2734
|
+
label: t.dict.action.delete,
|
|
2735
|
+
disabled: !current.local,
|
|
2736
|
+
run: () => doDelete(current.entry.id)
|
|
2737
|
+
},
|
|
2738
|
+
{ label: t.common.cancel, run: () => setPanel(null) }
|
|
2739
|
+
] : [];
|
|
2740
|
+
const morePanelItems = [
|
|
2741
|
+
{
|
|
2742
|
+
label: t.dict.command.pull,
|
|
2743
|
+
disabled: !current,
|
|
2744
|
+
run: () => current && doPull(current.entry.id)
|
|
2745
|
+
},
|
|
2746
|
+
{
|
|
2747
|
+
label: t.dict.command.import,
|
|
2748
|
+
disabled: true,
|
|
2749
|
+
run: () => void 0
|
|
2750
|
+
},
|
|
2751
|
+
{ label: t.dict.command.refreshList, run: () => doRefreshList() },
|
|
2752
|
+
{ label: t.common.cancel, run: () => setPanel(null) }
|
|
2753
|
+
];
|
|
2754
|
+
if (panel === "item" && current) {
|
|
2755
|
+
return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(
|
|
2756
|
+
ActionPanel,
|
|
2757
|
+
{
|
|
2758
|
+
title: `${t.dict.action.title} \xB7 ${current.entry.name}`,
|
|
2759
|
+
items: itemPanelItems,
|
|
2760
|
+
onClose: () => setPanel(null)
|
|
2761
|
+
}
|
|
2762
|
+
) });
|
|
2763
|
+
}
|
|
2764
|
+
if (panel === "more") {
|
|
2765
|
+
return /* @__PURE__ */ jsx12(Box7, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx12(
|
|
2766
|
+
ActionPanel,
|
|
2767
|
+
{
|
|
2768
|
+
title: t.dict.command.title,
|
|
2769
|
+
items: morePanelItems,
|
|
2770
|
+
onClose: () => setPanel(null)
|
|
2771
|
+
}
|
|
2772
|
+
) });
|
|
2773
|
+
}
|
|
2774
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2775
|
+
/* @__PURE__ */ jsxs5(Box7, { children: [
|
|
2776
|
+
/* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.accent, children: t.dict.title }),
|
|
2777
|
+
/* @__PURE__ */ jsx12(Box7, { flexGrow: 1 }),
|
|
2778
|
+
/* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: filter ? `${t.dict.filterPlaceholder}: ${filter}_` : `${t.dict.filterPlaceholder}_` }),
|
|
2779
|
+
/* @__PURE__ */ jsxs5(Text6, { color: PALETTE.muted, children: [
|
|
2780
|
+
" ",
|
|
2781
|
+
t.dict.entries(filtered.length)
|
|
2782
|
+
] })
|
|
2201
2783
|
] }),
|
|
2202
|
-
/* @__PURE__ */
|
|
2203
|
-
/* @__PURE__ */
|
|
2204
|
-
const i =
|
|
2784
|
+
/* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexGrow: 1, children: [
|
|
2785
|
+
/* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "75%", paddingRight: 1, children: filtered.slice(start2, end).map((row, vi) => {
|
|
2786
|
+
const i = start2 + vi;
|
|
2205
2787
|
const active = i === safeSelected;
|
|
2206
2788
|
const isDefault = cfg.defaultDict === row.entry.id;
|
|
2207
|
-
return /* @__PURE__ */
|
|
2208
|
-
/* @__PURE__ */
|
|
2209
|
-
/* @__PURE__ */
|
|
2210
|
-
/* @__PURE__ */
|
|
2211
|
-
/* @__PURE__ */
|
|
2212
|
-
/* @__PURE__ */
|
|
2789
|
+
return /* @__PURE__ */ jsxs5(Box7, { children: [
|
|
2790
|
+
/* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }) }),
|
|
2791
|
+
/* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: row.local ? PALETTE.accent : PALETTE.muted, children: row.local ? "\u25CF" : "\u25CB" }) }),
|
|
2792
|
+
/* @__PURE__ */ jsx12(Box7, { width: 2, children: /* @__PURE__ */ jsx12(Text6, { color: isDefault ? PALETTE.success : PALETTE.muted, children: isDefault ? "\u2605" : " " }) }),
|
|
2793
|
+
/* @__PURE__ */ jsx12(Box7, { flexGrow: 1, children: /* @__PURE__ */ jsx12(Text6, { bold: active, color: active ? PALETTE.text : PALETTE.muted, wrap: "truncate", children: row.entry.name }) }),
|
|
2794
|
+
/* @__PURE__ */ jsx12(Box7, { width: 6, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: String(row.entry.length).padStart(5) }) })
|
|
2213
2795
|
] }, row.entry.id);
|
|
2214
2796
|
}) }),
|
|
2215
|
-
/* @__PURE__ */
|
|
2216
|
-
/* @__PURE__ */
|
|
2217
|
-
/* @__PURE__ */
|
|
2218
|
-
/* @__PURE__ */
|
|
2797
|
+
/* @__PURE__ */ jsx12(Box7, { flexDirection: "column", width: "25%", paddingLeft: 1, children: current && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
2798
|
+
/* @__PURE__ */ jsx12(Text6, { bold: true, color: PALETTE.text, wrap: "wrap", children: current.entry.name }),
|
|
2799
|
+
/* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: current.entry.id }),
|
|
2800
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: PALETTE.muted, wrap: "wrap", children: [
|
|
2219
2801
|
current.entry.language,
|
|
2220
2802
|
" \xB7 ",
|
|
2221
2803
|
current.entry.category
|
|
2222
2804
|
] }) }),
|
|
2223
|
-
/* @__PURE__ */
|
|
2224
|
-
current.entry.description && /* @__PURE__ */
|
|
2225
|
-
current.entry.tags.length > 0 && /* @__PURE__ */
|
|
2226
|
-
/* @__PURE__ */
|
|
2227
|
-
cfg.defaultDict === current.entry.id && /* @__PURE__ */
|
|
2805
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.wordsLabel(current.entry.length) }) }),
|
|
2806
|
+
current.entry.description && /* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.primary, wrap: "wrap", children: current.entry.description }) }),
|
|
2807
|
+
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(", ")) }) }),
|
|
2808
|
+
/* @__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 }) }),
|
|
2809
|
+
cfg.defaultDict === current.entry.id && /* @__PURE__ */ jsx12(Box7, { children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.success, children: t.dict.defaultMark }) })
|
|
2228
2810
|
] }) })
|
|
2229
2811
|
] }),
|
|
2230
|
-
pending && /* @__PURE__ */
|
|
2231
|
-
pending.kind === "pulling" && /* @__PURE__ */
|
|
2232
|
-
pending.kind === "removing" && /* @__PURE__ */
|
|
2233
|
-
pending.kind === "
|
|
2812
|
+
pending && /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, children: [
|
|
2813
|
+
pending.kind === "pulling" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.warning, children: t.dict.pulling(pending.id) }),
|
|
2814
|
+
pending.kind === "removing" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.warning, children: t.dict.removing(pending.id) }),
|
|
2815
|
+
pending.kind === "refreshing" && /* @__PURE__ */ jsxs5(Text6, { color: PALETTE.warning, children: [
|
|
2816
|
+
t.dict.command.refreshList,
|
|
2817
|
+
"\u2026"
|
|
2818
|
+
] }),
|
|
2819
|
+
pending.kind === "error" && /* @__PURE__ */ jsx12(Text6, { color: PALETTE.error, children: t.dict.errorOn(pending.id, pending.msg) })
|
|
2234
2820
|
] }),
|
|
2235
|
-
/* @__PURE__ */
|
|
2821
|
+
/* @__PURE__ */ jsx12(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text6, { color: PALETTE.muted, children: t.dict.footer }) })
|
|
2236
2822
|
] });
|
|
2237
2823
|
}
|
|
2238
2824
|
|
|
2239
2825
|
// src/ui/screens/ConfigEditor.tsx
|
|
2240
|
-
import { useState as
|
|
2241
|
-
import { Box as
|
|
2242
|
-
import { jsx as
|
|
2826
|
+
import { useState as useState11 } from "react";
|
|
2827
|
+
import { Box as Box8, Text as Text7, useInput as useInput6 } from "ink";
|
|
2828
|
+
import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2243
2829
|
var FIELDS = [
|
|
2244
2830
|
{ kind: "dictRef", path: "defaultDict", labelKey: "defaultDict" },
|
|
2245
2831
|
{ kind: "enum", path: "defaultMode", labelKey: "defaultMode", options: ["order", "dictation", "review", "random", "loop"] },
|
|
2246
2832
|
{ kind: "enum", path: "accent", labelKey: "accent", options: ["us", "uk"] },
|
|
2247
2833
|
{ kind: "enum", path: "language", labelKey: "language", options: ["auto", "zh", "en"] },
|
|
2248
2834
|
{ kind: "enum", path: "mirror", labelKey: "mirror", options: ["jsdelivr", "github"] },
|
|
2835
|
+
{ kind: "enum", path: "stealth", labelKey: "stealth", options: ["off", "menu", "default"] },
|
|
2249
2836
|
{ kind: "int", path: "chapterSize", labelKey: "chapterSize", min: 1, max: 200 },
|
|
2250
2837
|
{ kind: "bool", path: "autoplayPronunciation", labelKey: "autoplayPronunciation" },
|
|
2251
2838
|
{ kind: "bool", path: "sounds.master", labelKey: "soundsMaster" },
|
|
@@ -2263,10 +2850,11 @@ function ConfigEditor() {
|
|
|
2263
2850
|
const nav = useNav();
|
|
2264
2851
|
const { cfg, setCfg } = useAppState();
|
|
2265
2852
|
const t = useStrings();
|
|
2266
|
-
const
|
|
2267
|
-
const [
|
|
2268
|
-
const [
|
|
2269
|
-
const [
|
|
2853
|
+
const defaultDictName = useDictName(cfg.defaultDict);
|
|
2854
|
+
const [selected, setSelected] = useState11(0);
|
|
2855
|
+
const [editing, setEditing] = useState11(false);
|
|
2856
|
+
const [draft, setDraft] = useState11("");
|
|
2857
|
+
const [error, setError] = useState11(null);
|
|
2270
2858
|
const field = FIELDS[selected];
|
|
2271
2859
|
const currentValue = getByPath2(cfg, field.path);
|
|
2272
2860
|
const commit = async (raw) => {
|
|
@@ -2279,7 +2867,7 @@ function ConfigEditor() {
|
|
|
2279
2867
|
setError(err.message);
|
|
2280
2868
|
}
|
|
2281
2869
|
};
|
|
2282
|
-
|
|
2870
|
+
useInput6((input, key) => {
|
|
2283
2871
|
if (editing && field.kind === "string") {
|
|
2284
2872
|
if (key.escape) {
|
|
2285
2873
|
setEditing(false);
|
|
@@ -2314,7 +2902,7 @@ function ConfigEditor() {
|
|
|
2314
2902
|
if (/^[0-9]$/.test(input)) setDraft((d) => d + input);
|
|
2315
2903
|
return;
|
|
2316
2904
|
}
|
|
2317
|
-
if (key.escape
|
|
2905
|
+
if (key.escape) {
|
|
2318
2906
|
nav.back();
|
|
2319
2907
|
return;
|
|
2320
2908
|
}
|
|
@@ -2350,35 +2938,51 @@ function ConfigEditor() {
|
|
|
2350
2938
|
}
|
|
2351
2939
|
});
|
|
2352
2940
|
const labelW = Math.max(...FIELDS.map((f) => visibleWidth2(t.config.fields[f.labelKey]))) + 4;
|
|
2353
|
-
return /* @__PURE__ */
|
|
2354
|
-
/* @__PURE__ */
|
|
2355
|
-
/* @__PURE__ */
|
|
2941
|
+
return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
2942
|
+
/* @__PURE__ */ jsx13(Text7, { bold: true, color: PALETTE.accent, children: t.config.title }),
|
|
2943
|
+
/* @__PURE__ */ jsx13(Box8, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: FIELDS.map((f, i) => {
|
|
2356
2944
|
const active = i === selected;
|
|
2357
2945
|
const value = getByPath2(cfg, f.path);
|
|
2358
|
-
const display = renderValue(
|
|
2946
|
+
const display = renderValue(
|
|
2947
|
+
f,
|
|
2948
|
+
value,
|
|
2949
|
+
active && editing ? draft : null,
|
|
2950
|
+
t,
|
|
2951
|
+
f.path === "defaultDict" ? defaultDictName : ""
|
|
2952
|
+
);
|
|
2359
2953
|
const label = t.config.fields[f.labelKey];
|
|
2360
2954
|
const pad = " ".repeat(Math.max(0, labelW - visibleWidth2(label)));
|
|
2361
|
-
return /* @__PURE__ */
|
|
2362
|
-
/* @__PURE__ */
|
|
2363
|
-
/* @__PURE__ */
|
|
2955
|
+
return /* @__PURE__ */ jsxs6(Box8, { children: [
|
|
2956
|
+
/* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
2957
|
+
/* @__PURE__ */ jsxs6(Text7, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: [
|
|
2364
2958
|
label,
|
|
2365
2959
|
pad
|
|
2366
2960
|
] }),
|
|
2367
|
-
/* @__PURE__ */
|
|
2961
|
+
/* @__PURE__ */ jsx13(Text7, { color: active ? PALETTE.accent : PALETTE.muted, children: display })
|
|
2368
2962
|
] }, f.path);
|
|
2369
2963
|
}) }),
|
|
2370
|
-
error && /* @__PURE__ */
|
|
2964
|
+
error && /* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: PALETTE.error, children: [
|
|
2371
2965
|
"! ",
|
|
2372
2966
|
error
|
|
2373
2967
|
] }) }),
|
|
2374
|
-
/* @__PURE__ */
|
|
2968
|
+
/* @__PURE__ */ jsx13(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text7, { color: PALETTE.muted, children: hintFor(field, editing, t) }) })
|
|
2375
2969
|
] });
|
|
2376
2970
|
}
|
|
2377
|
-
function renderValue(field, value, draft, t) {
|
|
2971
|
+
function renderValue(field, value, draft, t, dictDisplayName) {
|
|
2378
2972
|
if (draft !== null) return `${draft}_`;
|
|
2379
2973
|
if (field.kind === "bool") return value ? `\u2713 ${t.common.on}` : `\u2717 ${t.common.off}`;
|
|
2380
|
-
if (field.kind === "dictRef")
|
|
2381
|
-
|
|
2974
|
+
if (field.kind === "dictRef") {
|
|
2975
|
+
if (!value) return "\u2014";
|
|
2976
|
+
return truncateName(dictDisplayName || String(value), 24);
|
|
2977
|
+
}
|
|
2978
|
+
if (field.kind === "enum") {
|
|
2979
|
+
if (field.path === "stealth") {
|
|
2980
|
+
const v = String(value);
|
|
2981
|
+
const label = t.config.enumValues.stealth[v] ?? String(value);
|
|
2982
|
+
return `< ${label} >`;
|
|
2983
|
+
}
|
|
2984
|
+
return `< ${value} >`;
|
|
2985
|
+
}
|
|
2382
2986
|
return String(value ?? "");
|
|
2383
2987
|
}
|
|
2384
2988
|
function hintFor(field, editing, t) {
|
|
@@ -2390,46 +2994,119 @@ function hintFor(field, editing, t) {
|
|
|
2390
2994
|
}
|
|
2391
2995
|
|
|
2392
2996
|
// src/ui/screens/StatsViewer.tsx
|
|
2393
|
-
import { useEffect as
|
|
2394
|
-
import { Box as
|
|
2395
|
-
|
|
2997
|
+
import { useEffect as useEffect8, useState as useState12 } from "react";
|
|
2998
|
+
import { Box as Box10, Text as Text9, useInput as useInput7 } from "ink";
|
|
2999
|
+
|
|
3000
|
+
// src/ui/components/Heatmap.tsx
|
|
3001
|
+
import { Box as Box9, Text as Text8 } from "ink";
|
|
3002
|
+
|
|
3003
|
+
// src/util/heatmap.ts
|
|
3004
|
+
function bucketDays(values, days, todayWeekday) {
|
|
3005
|
+
if (days <= 0 || values.length !== days) {
|
|
3006
|
+
return Array.from({ length: 7 }, () => []);
|
|
3007
|
+
}
|
|
3008
|
+
const cols = Math.ceil((days - todayWeekday - 1) / 7) + 1;
|
|
3009
|
+
const grid = Array.from({ length: 7 }, () => Array(cols).fill(null));
|
|
3010
|
+
let col = cols - 1;
|
|
3011
|
+
let row = todayWeekday;
|
|
3012
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
3013
|
+
grid[row][col] = values[i];
|
|
3014
|
+
if (row === 0) {
|
|
3015
|
+
row = 6;
|
|
3016
|
+
col -= 1;
|
|
3017
|
+
} else {
|
|
3018
|
+
row -= 1;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return grid;
|
|
3022
|
+
}
|
|
3023
|
+
function intensity(value, max) {
|
|
3024
|
+
if (max <= 0 || value <= 0) return 0;
|
|
3025
|
+
const frac = value / max;
|
|
3026
|
+
if (frac <= 0.25) return 1;
|
|
3027
|
+
if (frac <= 0.5) return 2;
|
|
3028
|
+
if (frac <= 0.75) return 3;
|
|
3029
|
+
return 4;
|
|
3030
|
+
}
|
|
3031
|
+
function todayWeekdayMon0(now = /* @__PURE__ */ new Date()) {
|
|
3032
|
+
const d = now.getUTCDay();
|
|
3033
|
+
return d === 0 ? 6 : d - 1;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
// src/ui/components/Heatmap.tsx
|
|
3037
|
+
import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3038
|
+
var COLORS = ["#1b1f23", "#0e4429", "#006d32", "#26a641", "#39d353"];
|
|
3039
|
+
var ROW_LABELS = ["M", "T", "W", "T", "F", "S", "S"];
|
|
3040
|
+
function Heatmap({ label, values, days, suffix = "" }) {
|
|
3041
|
+
const todayDow = todayWeekdayMon0();
|
|
3042
|
+
const grid = bucketDays(values, days, todayDow);
|
|
3043
|
+
const max = values.length === 0 ? 0 : Math.max(...values);
|
|
3044
|
+
const cols = grid[0]?.length ?? 0;
|
|
3045
|
+
const maxLabel = max > 0 ? `max ${Math.round(max)}${suffix}` : "";
|
|
3046
|
+
return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", children: [
|
|
3047
|
+
/* @__PURE__ */ jsxs7(Box9, { children: [
|
|
3048
|
+
/* @__PURE__ */ jsx14(Box9, { width: 12, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: label }) }),
|
|
3049
|
+
/* @__PURE__ */ jsx14(Box9, { flexGrow: 1 }),
|
|
3050
|
+
maxLabel && /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: maxLabel })
|
|
3051
|
+
] }),
|
|
3052
|
+
grid.map((row, r) => /* @__PURE__ */ jsxs7(Box9, { children: [
|
|
3053
|
+
/* @__PURE__ */ jsx14(Box9, { width: 3, children: /* @__PURE__ */ jsx14(Text8, { color: PALETTE.muted, children: ROW_LABELS[r] }) }),
|
|
3054
|
+
row.map((cell, c) => {
|
|
3055
|
+
if (cell === null) {
|
|
3056
|
+
return /* @__PURE__ */ jsxs7(Text8, { children: [
|
|
3057
|
+
" ",
|
|
3058
|
+
c < cols - 1 ? " " : ""
|
|
3059
|
+
] }, c);
|
|
3060
|
+
}
|
|
3061
|
+
const lvl = intensity(cell, max);
|
|
3062
|
+
const sym = lvl === 0 ? "\xB7" : "\u25A0";
|
|
3063
|
+
return /* @__PURE__ */ jsxs7(Text8, { color: COLORS[lvl], children: [
|
|
3064
|
+
sym,
|
|
3065
|
+
c < cols - 1 ? " " : ""
|
|
3066
|
+
] }, c);
|
|
3067
|
+
})
|
|
3068
|
+
] }, r))
|
|
3069
|
+
] });
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// src/ui/screens/StatsViewer.tsx
|
|
3073
|
+
import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2396
3074
|
var DAY_WINDOWS = [7, 14, 30, 90];
|
|
2397
3075
|
function StatsViewer() {
|
|
2398
3076
|
const nav = useNav();
|
|
2399
3077
|
const t = useStrings();
|
|
2400
|
-
const [sessions, setSessions] =
|
|
2401
|
-
const [book, setBook] =
|
|
2402
|
-
const [windowIdx, setWindowIdx] =
|
|
2403
|
-
|
|
3078
|
+
const [sessions, setSessions] = useState12(null);
|
|
3079
|
+
const [book, setBook] = useState12(null);
|
|
3080
|
+
const [windowIdx, setWindowIdx] = useState12(1);
|
|
3081
|
+
useEffect8(() => {
|
|
2404
3082
|
void (async () => {
|
|
2405
3083
|
const [s, b] = await Promise.all([loadSessions(), loadMistakes()]);
|
|
2406
3084
|
setSessions(s);
|
|
2407
3085
|
setBook(b);
|
|
2408
3086
|
})();
|
|
2409
3087
|
}, []);
|
|
2410
|
-
|
|
2411
|
-
if (key.escape
|
|
3088
|
+
useInput7((_input, key) => {
|
|
3089
|
+
if (key.escape) {
|
|
2412
3090
|
nav.back();
|
|
2413
3091
|
return;
|
|
2414
3092
|
}
|
|
2415
|
-
if (
|
|
2416
|
-
if (
|
|
3093
|
+
if (key.rightArrow) setWindowIdx((i) => (i + 1) % DAY_WINDOWS.length);
|
|
3094
|
+
if (key.leftArrow) setWindowIdx((i) => (i - 1 + DAY_WINDOWS.length) % DAY_WINDOWS.length);
|
|
2417
3095
|
});
|
|
2418
3096
|
if (!sessions || !book) {
|
|
2419
|
-
return /* @__PURE__ */
|
|
3097
|
+
return /* @__PURE__ */ jsx15(Box10, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.loading }) });
|
|
2420
3098
|
}
|
|
2421
3099
|
if (sessions.length === 0) {
|
|
2422
|
-
return /* @__PURE__ */
|
|
2423
|
-
/* @__PURE__ */
|
|
2424
|
-
/* @__PURE__ */
|
|
2425
|
-
/* @__PURE__ */
|
|
2426
|
-
"
|
|
3100
|
+
return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
3101
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.none }),
|
|
3102
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.nonePractice }) }),
|
|
3103
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 2, children: /* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
|
|
3104
|
+
"Esc ",
|
|
2427
3105
|
t.common.back
|
|
2428
3106
|
] }) })
|
|
2429
3107
|
] });
|
|
2430
3108
|
}
|
|
2431
3109
|
const days = DAY_WINDOWS[windowIdx];
|
|
2432
|
-
const buckets = dailyBuckets(sessions, days);
|
|
2433
3110
|
const streak = dailyStreak(sessions);
|
|
2434
3111
|
const totalWords = sessions.reduce((a, s) => a + s.wordCount, 0);
|
|
2435
3112
|
const totalErrors = sessions.reduce((a, s) => a + s.errors, 0);
|
|
@@ -2442,98 +3119,122 @@ function StatsViewer() {
|
|
|
2442
3119
|
const overallAcc = totalWords === 0 ? 1 : firstTryWords / totalWords;
|
|
2443
3120
|
const recent = sessions.slice(-5).reverse();
|
|
2444
3121
|
const top = topN(book, 8);
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
/* @__PURE__ */
|
|
2454
|
-
/* @__PURE__ */
|
|
2455
|
-
/* @__PURE__ */
|
|
3122
|
+
const wpms = dailyValues(sessions, days, "wpm");
|
|
3123
|
+
const accs = dailyValues(sessions, days, "accuracy");
|
|
3124
|
+
const ses = dailyValues(sessions, days, "sessions");
|
|
3125
|
+
return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
3126
|
+
/* @__PURE__ */ jsx15(Text9, { bold: true, color: PALETTE.accent, children: t.stats.title }),
|
|
3127
|
+
/* @__PURE__ */ jsxs8(Box10, { marginTop: 1, flexDirection: "column", children: [
|
|
3128
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.lifetime }),
|
|
3129
|
+
/* @__PURE__ */ jsxs8(Box10, { marginTop: 1, children: [
|
|
3130
|
+
/* @__PURE__ */ jsx15(Stat, { label: t.stats.sessions, value: String(sessions.length) }),
|
|
3131
|
+
/* @__PURE__ */ jsx15(Stat, { label: t.stats.words, value: String(totalWords) }),
|
|
3132
|
+
/* @__PURE__ */ jsx15(Stat, { label: t.stats.errors, value: String(totalErrors) }),
|
|
3133
|
+
/* @__PURE__ */ jsx15(Stat, { label: t.stats.wpm, value: String(overallWpm), accent: true }),
|
|
3134
|
+
/* @__PURE__ */ jsx15(Stat, { label: t.stats.accuracy, value: `${Math.round(overallAcc * 1e3) / 10}%`, accent: true }),
|
|
3135
|
+
/* @__PURE__ */ jsx15(Stat, { label: t.stats.streak, value: `${streak}d`, accent: true })
|
|
2456
3136
|
] })
|
|
2457
3137
|
] }),
|
|
2458
|
-
/* @__PURE__ */
|
|
2459
|
-
/* @__PURE__ */
|
|
2460
|
-
/* @__PURE__ */
|
|
2461
|
-
/* @__PURE__ */
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
/* @__PURE__ */ jsxs6(Text6, { color: PALETTE.muted, children: [
|
|
2465
|
-
" max ",
|
|
2466
|
-
Math.round(Math.max(...buckets.map((b) => b.wpm)))
|
|
2467
|
-
] })
|
|
2468
|
-
] }),
|
|
2469
|
-
/* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2470
|
-
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.accuracy.padEnd(10) }),
|
|
2471
|
-
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.accuracy * 100)) })
|
|
2472
|
-
] }),
|
|
2473
|
-
/* @__PURE__ */ jsxs6(Box7, { children: [
|
|
2474
|
-
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.muted, children: t.stats.sessions.padEnd(10) }),
|
|
2475
|
-
/* @__PURE__ */ jsx11(Text6, { color: PALETTE.accent, children: sparkline(buckets.map((b) => b.sessions)) })
|
|
2476
|
-
] })
|
|
3138
|
+
/* @__PURE__ */ jsxs8(Box10, { marginTop: 2, flexDirection: "column", children: [
|
|
3139
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.last(days) }),
|
|
3140
|
+
/* @__PURE__ */ jsxs8(Box10, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: PALETTE.muted, paddingX: 1, paddingY: 0, children: [
|
|
3141
|
+
/* @__PURE__ */ jsx15(Heatmap, { label: t.stats.wpm, values: wpms, days }),
|
|
3142
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Heatmap, { label: t.stats.accuracy, values: accs, days, suffix: "%" }) }),
|
|
3143
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Heatmap, { label: t.stats.sessions, values: ses, days }) })
|
|
2477
3144
|
] })
|
|
2478
3145
|
] }),
|
|
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))
|
|
3146
|
+
/* @__PURE__ */ jsxs8(Box10, { marginTop: 2, flexDirection: "column", children: [
|
|
3147
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.recent }),
|
|
3148
|
+
recent.map((s, i) => /* @__PURE__ */ jsx15(RecentRow, { session: s, units: t.stats.recentUnits }, i))
|
|
2505
3149
|
] }),
|
|
2506
|
-
top.length > 0 && /* @__PURE__ */
|
|
2507
|
-
/* @__PURE__ */
|
|
2508
|
-
top.map(([word, entry]) => /* @__PURE__ */
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
3150
|
+
top.length > 0 && /* @__PURE__ */ jsxs8(Box10, { marginTop: 2, flexDirection: "column", children: [
|
|
3151
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.topMistakes }),
|
|
3152
|
+
top.map(([word, entry]) => /* @__PURE__ */ jsx15(
|
|
3153
|
+
MistakeRow,
|
|
3154
|
+
{
|
|
3155
|
+
word,
|
|
3156
|
+
count: entry.count,
|
|
3157
|
+
dictIds: entry.dictIds,
|
|
3158
|
+
multiSuffix: t.stats.multiDictSuffix
|
|
3159
|
+
},
|
|
3160
|
+
word
|
|
3161
|
+
))
|
|
2517
3162
|
] }),
|
|
2518
|
-
/* @__PURE__ */
|
|
2519
|
-
/* @__PURE__ */
|
|
3163
|
+
/* @__PURE__ */ jsx15(Box10, { flexGrow: 1 }),
|
|
3164
|
+
/* @__PURE__ */ jsx15(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text9, { color: PALETTE.muted, children: t.stats.footer }) })
|
|
3165
|
+
] });
|
|
3166
|
+
}
|
|
3167
|
+
function RecentRow({
|
|
3168
|
+
session,
|
|
3169
|
+
units
|
|
3170
|
+
}) {
|
|
3171
|
+
const name = useDictName(session.dictId);
|
|
3172
|
+
const display = truncateName(name, 14);
|
|
3173
|
+
return /* @__PURE__ */ jsxs8(Box10, { children: [
|
|
3174
|
+
/* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
|
|
3175
|
+
" ",
|
|
3176
|
+
session.ts.replace("T", " ").slice(0, 16),
|
|
3177
|
+
" "
|
|
3178
|
+
] }),
|
|
3179
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.text, children: display.padEnd(14) }),
|
|
3180
|
+
/* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
|
|
3181
|
+
" ",
|
|
3182
|
+
"ch",
|
|
3183
|
+
String(session.chapter + 1).padStart(3),
|
|
3184
|
+
" ",
|
|
3185
|
+
session.mode.padEnd(9),
|
|
3186
|
+
" ",
|
|
3187
|
+
String(session.wordCount).padStart(3),
|
|
3188
|
+
units.words,
|
|
3189
|
+
" ",
|
|
3190
|
+
session.errors,
|
|
3191
|
+
units.errors,
|
|
3192
|
+
" ",
|
|
3193
|
+
computeWPM(session),
|
|
3194
|
+
units.wpm,
|
|
3195
|
+
" ",
|
|
3196
|
+
Math.round(accuracy(session) * 1e3) / 10,
|
|
3197
|
+
"%"
|
|
3198
|
+
] })
|
|
3199
|
+
] });
|
|
3200
|
+
}
|
|
3201
|
+
function MistakeRow({
|
|
3202
|
+
word,
|
|
3203
|
+
count,
|
|
3204
|
+
dictIds,
|
|
3205
|
+
multiSuffix
|
|
3206
|
+
}) {
|
|
3207
|
+
const firstId = dictIds[0] ?? "";
|
|
3208
|
+
const firstName = useDictName(firstId);
|
|
3209
|
+
const suffix = dictIds.length > 1 ? multiSuffix(dictIds.length - 1) : "";
|
|
3210
|
+
return /* @__PURE__ */ jsxs8(Box10, { children: [
|
|
3211
|
+
/* @__PURE__ */ jsxs8(Text9, { color: PALETTE.error, children: [
|
|
3212
|
+
" ",
|
|
3213
|
+
String(count).padStart(3),
|
|
3214
|
+
" "
|
|
3215
|
+
] }),
|
|
3216
|
+
/* @__PURE__ */ jsx15(Text9, { color: PALETTE.text, children: word.padEnd(20) }),
|
|
3217
|
+
/* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
|
|
3218
|
+
truncateName(firstName, 20),
|
|
3219
|
+
suffix
|
|
3220
|
+
] })
|
|
2520
3221
|
] });
|
|
2521
3222
|
}
|
|
2522
3223
|
function Stat({ label, value, accent = false }) {
|
|
2523
|
-
return /* @__PURE__ */
|
|
2524
|
-
/* @__PURE__ */
|
|
3224
|
+
return /* @__PURE__ */ jsxs8(Box10, { marginRight: 3, children: [
|
|
3225
|
+
/* @__PURE__ */ jsxs8(Text9, { color: PALETTE.muted, children: [
|
|
2525
3226
|
label,
|
|
2526
3227
|
" "
|
|
2527
3228
|
] }),
|
|
2528
|
-
/* @__PURE__ */
|
|
3229
|
+
/* @__PURE__ */ jsx15(Text9, { bold: true, color: accent ? PALETTE.accent : PALETTE.text, children: value })
|
|
2529
3230
|
] });
|
|
2530
3231
|
}
|
|
2531
3232
|
|
|
2532
3233
|
// src/ui/screens/WordLookup.tsx
|
|
2533
|
-
import { useEffect as
|
|
2534
|
-
import { Box as
|
|
3234
|
+
import { useEffect as useEffect9, useState as useState13 } from "react";
|
|
3235
|
+
import { Box as Box11, Text as Text10, useInput as useInput8 } from "ink";
|
|
2535
3236
|
import { readdir } from "fs/promises";
|
|
2536
|
-
import { Fragment as Fragment2, jsx as
|
|
3237
|
+
import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2537
3238
|
async function listLocalDictIds() {
|
|
2538
3239
|
try {
|
|
2539
3240
|
const files = await readdir(paths.dictsDir);
|
|
@@ -2545,12 +3246,12 @@ async function listLocalDictIds() {
|
|
|
2545
3246
|
function WordLookup() {
|
|
2546
3247
|
const nav = useNav();
|
|
2547
3248
|
const t = useStrings();
|
|
2548
|
-
const [query, setQuery] =
|
|
2549
|
-
const [allWords, setAllWords] =
|
|
2550
|
-
const [book, setBook] =
|
|
2551
|
-
const [loading, setLoading] =
|
|
2552
|
-
const [selected, setSelected] =
|
|
2553
|
-
|
|
3249
|
+
const [query, setQuery] = useState13("");
|
|
3250
|
+
const [allWords, setAllWords] = useState13([]);
|
|
3251
|
+
const [book, setBook] = useState13({});
|
|
3252
|
+
const [loading, setLoading] = useState13(true);
|
|
3253
|
+
const [selected, setSelected] = useState13(0);
|
|
3254
|
+
useEffect9(() => {
|
|
2554
3255
|
void (async () => {
|
|
2555
3256
|
const ids = await listLocalDictIds();
|
|
2556
3257
|
const collected = [];
|
|
@@ -2566,7 +3267,7 @@ function WordLookup() {
|
|
|
2566
3267
|
}, []);
|
|
2567
3268
|
const q = query.toLowerCase().trim();
|
|
2568
3269
|
const filtered = q ? allWords.filter((h) => h.word.name.toLowerCase().includes(q)).slice(0, 50) : [];
|
|
2569
|
-
|
|
3270
|
+
useInput8((input, key) => {
|
|
2570
3271
|
if (key.escape) {
|
|
2571
3272
|
nav.back();
|
|
2572
3273
|
return;
|
|
@@ -2590,111 +3291,165 @@ function WordLookup() {
|
|
|
2590
3291
|
}
|
|
2591
3292
|
});
|
|
2592
3293
|
if (loading) {
|
|
2593
|
-
return /* @__PURE__ */
|
|
3294
|
+
return /* @__PURE__ */ jsx16(Box11, { alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.indexing }) });
|
|
2594
3295
|
}
|
|
2595
3296
|
if (allWords.length === 0) {
|
|
2596
|
-
return /* @__PURE__ */
|
|
2597
|
-
/* @__PURE__ */
|
|
2598
|
-
/* @__PURE__ */
|
|
2599
|
-
/* @__PURE__ */
|
|
3297
|
+
return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", children: [
|
|
3298
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.none }),
|
|
3299
|
+
/* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.pullFirst }) }),
|
|
3300
|
+
/* @__PURE__ */ jsx16(Box11, { marginTop: 2, children: /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.muted, children: [
|
|
2600
3301
|
"[Esc] ",
|
|
2601
3302
|
t.common.back
|
|
2602
3303
|
] }) })
|
|
2603
3304
|
] });
|
|
2604
3305
|
}
|
|
2605
3306
|
const current = filtered[selected];
|
|
2606
|
-
return /* @__PURE__ */
|
|
2607
|
-
/* @__PURE__ */
|
|
2608
|
-
/* @__PURE__ */
|
|
2609
|
-
/* @__PURE__ */
|
|
2610
|
-
/* @__PURE__ */
|
|
3307
|
+
return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
3308
|
+
/* @__PURE__ */ jsxs9(Box11, { children: [
|
|
3309
|
+
/* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.accent, children: t.word.title }),
|
|
3310
|
+
/* @__PURE__ */ jsx16(Box11, { flexGrow: 1 }),
|
|
3311
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.countAcross(allWords.length) })
|
|
2611
3312
|
] }),
|
|
2612
|
-
/* @__PURE__ */
|
|
2613
|
-
/* @__PURE__ */
|
|
2614
|
-
/* @__PURE__ */
|
|
2615
|
-
/* @__PURE__ */
|
|
3313
|
+
/* @__PURE__ */ jsxs9(Box11, { marginTop: 1, children: [
|
|
3314
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: "> " }),
|
|
3315
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.text, children: query }),
|
|
3316
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.accent, children: "_" })
|
|
2616
3317
|
] }),
|
|
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) })
|
|
3318
|
+
/* @__PURE__ */ jsxs9(Box11, { marginTop: 1, flexGrow: 1, children: [
|
|
3319
|
+
/* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", width: "40%", children: [
|
|
3320
|
+
filtered.map((h, i) => /* @__PURE__ */ jsx16(HitRow, { hit: h, active: i === selected }, `${h.dictId}-${h.word.name}-${i}`)),
|
|
3321
|
+
filtered.length === 0 && q && /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.noMatches(query) })
|
|
2628
3322
|
] }),
|
|
2629
|
-
/* @__PURE__ */
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3323
|
+
/* @__PURE__ */ jsx16(Box11, { flexDirection: "column", width: "60%", paddingLeft: 2, children: current && /* @__PURE__ */ jsx16(Detail, { hit: current, book }) })
|
|
3324
|
+
] }),
|
|
3325
|
+
/* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.footer }) })
|
|
3326
|
+
] });
|
|
3327
|
+
}
|
|
3328
|
+
function HitRow({ hit, active }) {
|
|
3329
|
+
const name = useDictName(hit.dictId);
|
|
3330
|
+
return /* @__PURE__ */ jsxs9(Box11, { children: [
|
|
3331
|
+
/* @__PURE__ */ jsx16(Text10, { color: active ? PALETTE.accent : PALETTE.muted, children: active ? "\u258C " : " " }),
|
|
3332
|
+
/* @__PURE__ */ jsx16(Text10, { bold: active, color: active ? PALETTE.text : PALETTE.muted, children: hit.word.name.padEnd(20) }),
|
|
3333
|
+
/* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: truncateName(name, 18) })
|
|
3334
|
+
] });
|
|
3335
|
+
}
|
|
3336
|
+
function Detail({ hit, book }) {
|
|
3337
|
+
const t = useStrings();
|
|
3338
|
+
const name = useDictName(hit.dictId);
|
|
3339
|
+
return /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
3340
|
+
/* @__PURE__ */ jsx16(Text10, { bold: true, color: PALETTE.text, children: hit.word.name }),
|
|
3341
|
+
/* @__PURE__ */ jsxs9(Box11, { marginTop: 1, children: [
|
|
3342
|
+
hit.word.usphone && /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.muted, children: [
|
|
3343
|
+
"US /",
|
|
3344
|
+
hit.word.usphone,
|
|
3345
|
+
"/ "
|
|
3346
|
+
] }),
|
|
3347
|
+
hit.word.ukphone && /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.muted, children: [
|
|
3348
|
+
"UK /",
|
|
3349
|
+
hit.word.ukphone,
|
|
3350
|
+
"/"
|
|
3351
|
+
] })
|
|
2650
3352
|
] }),
|
|
2651
|
-
/* @__PURE__ */
|
|
3353
|
+
/* @__PURE__ */ jsx16(Box11, { marginTop: 1, flexDirection: "column", children: (hit.word.trans ?? []).map((tr, i) => /* @__PURE__ */ jsxs9(Text10, { color: PALETTE.primary, children: [
|
|
3354
|
+
"\xB7 ",
|
|
3355
|
+
tr
|
|
3356
|
+
] }, i)) }),
|
|
3357
|
+
/* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.muted, children: t.word.inDict(truncateName(name, 22)) }) }),
|
|
3358
|
+
book[hit.word.name] && /* @__PURE__ */ jsx16(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text10, { color: PALETTE.error, children: t.word.mistakes(book[hit.word.name].count, book[hit.word.name].lastSeen.slice(0, 10)) }) })
|
|
3359
|
+
] });
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// src/ui/screens/HelpScreen.tsx
|
|
3363
|
+
import { Box as Box12, Text as Text11, useInput as useInput9 } from "ink";
|
|
3364
|
+
import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3365
|
+
function HelpScreen() {
|
|
3366
|
+
const nav = useNav();
|
|
3367
|
+
const t = useStrings();
|
|
3368
|
+
useInput9((_input, key) => {
|
|
3369
|
+
if (key.escape) nav.back();
|
|
3370
|
+
});
|
|
3371
|
+
const k = t.help.keys;
|
|
3372
|
+
const sections = [
|
|
3373
|
+
{ title: t.help.sections.global, keys: [k.helpScreen, k.quit] },
|
|
3374
|
+
{ title: t.help.sections.main, keys: [k.navigate, k.select, k.letterJump, k.helpScreen] },
|
|
3375
|
+
{
|
|
3376
|
+
title: t.help.sections.practice,
|
|
3377
|
+
keys: [k.pause, k.skip, k.replay, k.resume, k.nextChapter, k.reviewMistakes, k.stealthToggle, k.backMenu]
|
|
3378
|
+
},
|
|
3379
|
+
{
|
|
3380
|
+
title: t.help.sections.dict,
|
|
3381
|
+
keys: [k.navigate, k.filter, k.itemActions, k.moreActions, k.backScreen]
|
|
3382
|
+
},
|
|
3383
|
+
{ title: t.help.sections.config, keys: [k.navigate, k.select, k.backMenu] },
|
|
3384
|
+
{ title: t.help.sections.stats, keys: [k.cycleWindow, k.backMenu] },
|
|
3385
|
+
{ title: t.help.sections.word, keys: [k.filter, k.navigate, k.backMenu] }
|
|
3386
|
+
];
|
|
3387
|
+
return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", paddingX: 2, paddingY: 1, width: "100%", height: "100%", children: [
|
|
3388
|
+
/* @__PURE__ */ jsxs10(Box12, { children: [
|
|
3389
|
+
/* @__PURE__ */ jsx17(Text11, { bold: true, color: PALETTE.accent, children: t.help.title }),
|
|
3390
|
+
/* @__PURE__ */ jsx17(Text11, { color: PALETTE.muted, children: " \xB7 " }),
|
|
3391
|
+
/* @__PURE__ */ jsx17(Text11, { color: PALETTE.muted, children: t.help.subtitle })
|
|
3392
|
+
] }),
|
|
3393
|
+
/* @__PURE__ */ jsx17(Box12, { marginTop: 1, flexDirection: "column", flexGrow: 1, children: sections.map((sec) => /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", marginTop: 1, children: [
|
|
3394
|
+
/* @__PURE__ */ jsx17(Text11, { bold: true, color: PALETTE.text, children: sec.title }),
|
|
3395
|
+
sec.keys.map((line, i) => /* @__PURE__ */ jsx17(Box12, { children: /* @__PURE__ */ jsxs10(Text11, { color: PALETTE.muted, children: [
|
|
3396
|
+
" \xB7 ",
|
|
3397
|
+
line
|
|
3398
|
+
] }) }, i))
|
|
3399
|
+
] }, sec.title)) }),
|
|
3400
|
+
/* @__PURE__ */ jsx17(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx17(Text11, { color: PALETTE.muted, children: t.help.footer }) })
|
|
2652
3401
|
] });
|
|
2653
3402
|
}
|
|
2654
3403
|
|
|
2655
3404
|
// src/ui/App.tsx
|
|
2656
|
-
import { jsx as
|
|
2657
|
-
function App({
|
|
2658
|
-
|
|
3405
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
3406
|
+
function App({
|
|
3407
|
+
initial,
|
|
3408
|
+
initialCfg,
|
|
3409
|
+
inline = false
|
|
3410
|
+
}) {
|
|
3411
|
+
return /* @__PURE__ */ jsx18(AppStateProvider, { initialCfg, children: /* @__PURE__ */ jsx18(LangBridge, { children: /* @__PURE__ */ jsx18(RegistryProvider, { children: /* @__PURE__ */ jsx18(AudioStatusProvider, { disabled: !initialCfg.sounds.master, children: /* @__PURE__ */ jsx18(NavProvider, { initial, children: inline ? /* @__PURE__ */ jsx18(Router, { inline: true }) : /* @__PURE__ */ jsx18(Fullscreen, { children: /* @__PURE__ */ jsx18(Router, {}) }) }) }) }) }) });
|
|
2659
3412
|
}
|
|
2660
3413
|
function LangBridge({ children }) {
|
|
2661
3414
|
const { cfg } = useAppState();
|
|
2662
|
-
return /* @__PURE__ */
|
|
3415
|
+
return /* @__PURE__ */ jsx18(StringsProvider, { pref: cfg.language, children });
|
|
2663
3416
|
}
|
|
2664
3417
|
function screenKey(frame) {
|
|
2665
3418
|
if (frame.name === "practice") {
|
|
2666
3419
|
const p = frame.params;
|
|
2667
|
-
return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}`;
|
|
3420
|
+
return `practice:${p.dictId}:${p.chapterIndex}:${p.mode}:${p.stealth ? "s" : "n"}`;
|
|
2668
3421
|
}
|
|
2669
3422
|
return frame.name;
|
|
2670
3423
|
}
|
|
2671
|
-
function Router() {
|
|
3424
|
+
function Router({ inline = false }) {
|
|
2672
3425
|
const nav = useNav();
|
|
2673
3426
|
const { cfg } = useAppState();
|
|
2674
3427
|
const { exit } = useApp4();
|
|
2675
3428
|
const lastKeyRef = useRef4(null);
|
|
2676
|
-
|
|
3429
|
+
useInput10((input, key2) => {
|
|
2677
3430
|
if (key2.ctrl && input === "c") exit();
|
|
2678
3431
|
});
|
|
2679
3432
|
const frame = nav.current;
|
|
2680
3433
|
const key = screenKey(frame);
|
|
2681
3434
|
if (lastKeyRef.current !== key) {
|
|
2682
|
-
if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[H");
|
|
3435
|
+
if (!inline && process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[H");
|
|
2683
3436
|
lastKeyRef.current = key;
|
|
2684
3437
|
}
|
|
2685
3438
|
switch (frame.name) {
|
|
2686
3439
|
case "main":
|
|
2687
|
-
return /* @__PURE__ */
|
|
3440
|
+
return /* @__PURE__ */ jsx18(MainMenu, { cfg });
|
|
2688
3441
|
case "practice":
|
|
2689
|
-
return /* @__PURE__ */
|
|
3442
|
+
return /* @__PURE__ */ jsx18(PracticeScreen, { params: frame.params });
|
|
2690
3443
|
case "dict":
|
|
2691
|
-
return /* @__PURE__ */
|
|
3444
|
+
return /* @__PURE__ */ jsx18(DictBrowser, { params: frame.params });
|
|
2692
3445
|
case "config":
|
|
2693
|
-
return /* @__PURE__ */
|
|
3446
|
+
return /* @__PURE__ */ jsx18(ConfigEditor, {});
|
|
2694
3447
|
case "stats":
|
|
2695
|
-
return /* @__PURE__ */
|
|
3448
|
+
return /* @__PURE__ */ jsx18(StatsViewer, {});
|
|
2696
3449
|
case "word":
|
|
2697
|
-
return /* @__PURE__ */
|
|
3450
|
+
return /* @__PURE__ */ jsx18(WordLookup, {});
|
|
3451
|
+
case "help":
|
|
3452
|
+
return /* @__PURE__ */ jsx18(HelpScreen, {});
|
|
2698
3453
|
}
|
|
2699
3454
|
}
|
|
2700
3455
|
|
|
@@ -2716,7 +3471,19 @@ function fmtDuration(ms, lang) {
|
|
|
2716
3471
|
return `${m}m ${s}s`;
|
|
2717
3472
|
}
|
|
2718
3473
|
function printSessionReport(r, t, lang) {
|
|
2719
|
-
if (r.chaptersCompleted === 0) return;
|
|
3474
|
+
if (r.startedAt === null && r.chaptersCompleted === 0) return;
|
|
3475
|
+
if (r.chaptersCompleted === 0) {
|
|
3476
|
+
console.log();
|
|
3477
|
+
console.log(chalk3.bold.cyan(t.report.title));
|
|
3478
|
+
const labelW2 = Math.max(visibleWidth2(t.report.duration), visibleWidth2(t.report.notPracticed)) + 2;
|
|
3479
|
+
const pad2 = (label) => label + " ".repeat(Math.max(0, labelW2 - visibleWidth2(label)));
|
|
3480
|
+
console.log(` ${chalk3.dim(pad2(t.report.duration))} ${fmtDuration(r.totalDurationMs, lang)}`);
|
|
3481
|
+
console.log(` ${chalk3.dim(t.report.notPracticed)}`);
|
|
3482
|
+
console.log();
|
|
3483
|
+
console.log(chalk3.dim(` ${t.report.farewell}`));
|
|
3484
|
+
console.log();
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
2720
3487
|
const accPct = Math.round(r.accuracy * 1e3) / 10;
|
|
2721
3488
|
const labels = [
|
|
2722
3489
|
t.report.duration,
|
|
@@ -2770,23 +3537,31 @@ async function runPractice(dictIdArg, options) {
|
|
|
2770
3537
|
return;
|
|
2771
3538
|
}
|
|
2772
3539
|
const chapterIndex = Math.max(0, Number(options.chapter ?? 1) - 1);
|
|
3540
|
+
const stealth = options.stealth === true || cfg.stealth === "default";
|
|
2773
3541
|
start();
|
|
2774
3542
|
const { waitUntilExit } = render(
|
|
2775
3543
|
createElement(App, {
|
|
2776
|
-
initial: { name: "practice", params: { dictId, chapterIndex, mode } },
|
|
2777
|
-
initialCfg: cfg
|
|
3544
|
+
initial: { name: "practice", params: { dictId, chapterIndex, mode, stealth } },
|
|
3545
|
+
initialCfg: cfg,
|
|
3546
|
+
inline: stealth
|
|
2778
3547
|
}),
|
|
2779
3548
|
{ patchConsole: false, exitOnCtrlC: false }
|
|
2780
3549
|
);
|
|
2781
3550
|
await waitUntilExit();
|
|
2782
|
-
|
|
3551
|
+
if (stealth && process.stdout.isTTY) {
|
|
3552
|
+
process.stdout.write("\x1B[3F\x1B[0J");
|
|
3553
|
+
} else {
|
|
3554
|
+
ensureMainScreen();
|
|
3555
|
+
}
|
|
2783
3556
|
const { lang, t } = pickStrings(cfg.language);
|
|
2784
3557
|
printSessionReport(report(), t, lang);
|
|
2785
3558
|
}
|
|
2786
3559
|
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
|
-
|
|
3560
|
+
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(
|
|
3561
|
+
async (dictIdArg, options) => {
|
|
3562
|
+
await runPractice(dictIdArg, options);
|
|
3563
|
+
}
|
|
3564
|
+
);
|
|
2790
3565
|
}
|
|
2791
3566
|
|
|
2792
3567
|
// src/commands/stats.ts
|
|
@@ -2910,6 +3685,16 @@ function buildWordCommand() {
|
|
|
2910
3685
|
});
|
|
2911
3686
|
}
|
|
2912
3687
|
|
|
3688
|
+
// src/commands/stealth.ts
|
|
3689
|
+
import { Command as Command6 } from "commander";
|
|
3690
|
+
function buildStealthCommand() {
|
|
3691
|
+
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(
|
|
3692
|
+
async (dictIdArg, options) => {
|
|
3693
|
+
await runPractice(dictIdArg, { ...options, stealth: true });
|
|
3694
|
+
}
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3697
|
+
|
|
2913
3698
|
// src/commands/menu.ts
|
|
2914
3699
|
import { render as render2 } from "ink";
|
|
2915
3700
|
import { createElement as createElement2 } from "react";
|
|
@@ -2931,9 +3716,10 @@ async function runMainMenu() {
|
|
|
2931
3716
|
}
|
|
2932
3717
|
|
|
2933
3718
|
// src/cli.ts
|
|
2934
|
-
var program = new
|
|
3719
|
+
var program = new Command7();
|
|
2935
3720
|
program.name("qwerty").description("Terminal clone of qwerty-learner \u2014 typing practice for English vocabulary").version(package_default.version);
|
|
2936
3721
|
program.addCommand(buildPracticeCommand());
|
|
3722
|
+
program.addCommand(buildStealthCommand());
|
|
2937
3723
|
program.addCommand(buildDictCommand());
|
|
2938
3724
|
program.addCommand(buildWordCommand());
|
|
2939
3725
|
program.addCommand(buildStatsCommand());
|