Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b73656a7
|
||
|
|
e77aa5c3b3 | ||
|
|
121b688afb | ||
|
|
727be5683a | ||
|
|
138bfe9759 | ||
|
|
0aba17d95b
|
||
|
|
a5d0b63792 | ||
|
|
15cb9fe918
|
||
|
|
db45a70ec2
|
||
|
|
9e79fa93c2 | ||
|
|
9fadbc8cdd
|
||
|
|
ffbbe698fc | ||
|
|
7b72dd2cc4
|
||
|
|
416cd7e9ac
|
||
|
|
eb749f8c19
|
||
|
|
65ac5a7728 | ||
|
|
df675cfbfe
|
||
|
|
e49b5f6589
|
||
|
|
0bbd8617f4 | ||
|
|
4e43c5c6e1 | ||
|
|
953abdf42a
|
||
|
|
fdb0a31fff
|
||
|
|
04b878dd93
|
||
|
|
07b32626a6 | ||
|
|
c68b73c8c1 | ||
|
|
b24008237b | ||
|
|
63eb228e4f | ||
|
|
ba0155e429 | ||
|
|
23af9c6464
|
||
|
|
8825cca642
|
||
|
|
bc73a02623 | ||
|
|
5096ca217b
|
||
|
|
285b19ccc3
|
||
|
|
7fbc36dc41
|
||
|
|
e93b696e71
|
||
|
|
7c161723cd | ||
|
|
b0fb39a8f5 | ||
|
|
a7a302bdd0 | ||
|
|
6a1d1211ae
|
||
|
|
7585d430ae
|
||
|
|
122ff9a3c5
|
||
|
|
786bd5af81
|
||
|
|
42d1384c68 | ||
|
|
1508863dbb | ||
|
|
4dd30f44d1
|
||
|
|
ab48a5678e | ||
|
|
2f21582666 | ||
|
|
383aed8bad | ||
|
|
f9c4e892dc | ||
|
|
7959e1a4e5 | ||
|
|
917d52dd94 | ||
|
|
05425cc5db | ||
|
|
8097ed27e5 | ||
|
|
76e43d7e6d | ||
|
|
4f0762e840 | ||
|
|
026d495fac | ||
|
|
dd76782d30
|
||
|
|
a8c16147ad | ||
|
|
186d4a0640 | ||
|
|
2831e05124 | ||
|
|
6841a37a22 | ||
|
|
7c1eac03dc | ||
|
|
e6dc9dfec5
|
||
|
|
6d69a56574 | ||
|
|
0c37c665a2 | ||
|
|
8ae5bde6a4
|
||
|
|
016a43b804 | ||
|
|
fb2d1a512b | ||
|
|
614a8ca912 | ||
|
|
9eecb7358b
|
||
|
|
1e5d7747b4 | ||
|
|
c14c690875
|
||
|
|
84f718043a | ||
|
|
99266294b8 | ||
|
|
c055359be1
|
||
|
|
ec5a147095 | ||
|
|
a0635f4360 | ||
|
|
c0a78ef008 | ||
|
|
87b01155df | ||
|
|
fc5c49e365 | ||
|
|
a20269e9f5 | ||
|
|
2ad491e95c
|
||
|
|
6fffcc731f | ||
|
|
5cc21113fd | ||
|
|
051140f910 | ||
|
|
6e945f0872 | ||
|
|
c2c25c0da6 | ||
|
|
556de61756
|
||
|
|
a98fb0fddf
|
||
|
|
e816b3b371 | ||
|
|
ed7d3f4c3d | ||
|
|
509dc5bf7f | ||
|
|
0a0aa3ec98
|
||
|
|
b87cc3cfdd
|
||
|
|
8cb3c8c90a
|
||
|
|
03ea903927
|
||
|
|
3aea42e6f8
|
||
|
|
88bb3edfa4
|
||
|
|
9445aef004
|
||
|
|
c01bcd9d0f
|
||
|
|
695613b1e7
|
||
|
|
20797772f8 | ||
|
|
72c312810a
|
||
|
|
b61f050e78
|
||
|
|
2a77ba9dd4 | ||
|
|
4ed878270f
|
||
|
|
1d1b0c7bb6
|
||
|
|
db61ce358d
|
||
|
|
e9778a945a | ||
|
|
7de73e16a1 | ||
|
|
273652f781 | ||
|
|
14c0086520
|
||
|
|
ff0bc0af6d | ||
|
|
00b1b79bf4 | ||
|
|
e11a5fea0d
|
||
|
|
f73fe179d0
|
||
|
|
2938e7a32a | ||
|
|
82f6b4705a | ||
|
|
a02c33dac4 | ||
|
|
2174e689a2 | ||
|
|
7d729cb60c | ||
|
|
d963def1a9 | ||
|
|
6fb3d2eb13 | ||
|
|
c5a77ac067 | ||
|
|
8ddc151435
|
||
|
|
b98d4d65c7 | ||
|
|
046e74ea91
|
||
|
|
8bf847503d | ||
|
|
47b5903392
|
||
|
|
bf85554d1e | ||
|
|
d74c7e1235
|
||
|
|
57ddd19953 | ||
|
|
ee25536d90 | ||
|
|
7b0fbdf254 | ||
|
|
2fefc83e3f | ||
|
|
dbdf578c68 | ||
|
|
441ecf3c04 | ||
|
|
a0dde4ee3e
|
||
|
|
fe4dacc1e7 | ||
|
|
b08cd0db35 | ||
|
|
bffb1c5982 | ||
|
|
5b8848518a | ||
|
|
176edd67f1
|
||
|
|
4d65dec340 | ||
|
|
6607c333bc
|
||
|
|
b2bbf1ae12
|
||
|
|
b204d4dd6e | ||
|
|
89ed675935 | ||
|
|
1995200e76
|
||
|
|
e876e483d6
|
||
|
|
6d2a72e13b | ||
|
|
0d7084c8aa | ||
|
|
95e0abc7b7
|
||
|
|
d9155ceacb
|
||
|
|
9d0585423c
|
||
|
|
455cfff90a
|
||
|
|
987b325edb | ||
|
|
08c6807cb1
|
||
|
|
18c6410f24
|
||
|
|
e223cf9b71
|
||
|
|
c718bb8b34
|
||
|
|
177f7e5fd6
|
||
|
|
20b50ec4e1 | ||
|
|
deae61b211 | ||
|
|
44959ed282
|
||
|
|
2398f5c030 | ||
|
|
8711cf1a48
|
||
|
|
9f4888293b
|
||
|
|
29332b103e
|
||
|
|
8b2cee2c58 | ||
|
|
2e2ee3f028 | ||
|
|
49b926e08c | ||
|
|
66f8ca4f80 | ||
|
|
6fe1e0fee4 | ||
|
|
0ab840b362 | ||
|
|
21a06c12fb | ||
|
|
14070acceb
|
||
|
|
7d81342f0f
|
||
|
|
4b7f750919 | ||
|
|
6ab3d823a4 | ||
|
|
8797719a09
|
||
|
|
8acc78cc1c | ||
|
|
321461c50f | ||
|
|
beeab564b4
|
||
|
|
cab7975a2b
|
||
|
|
846292809c
|
||
|
|
8712780d08 | ||
|
|
7b1a005a65 | ||
|
|
84c75f50aa
|
||
|
|
8b838f2c7d
|
||
|
|
db4139ba0b
|
||
|
|
6c251502b3 | ||
|
|
ae40934d3a | ||
|
|
cdb1475a54
|
||
|
|
a2e49b369b
|
||
|
|
7f13aed50a | ||
|
|
8b21a2bca8
|
||
|
|
925413adfe
|
||
|
|
d0644ab2eb | ||
|
|
d253710c2e | ||
|
|
c3df510e4f
|
||
|
|
187f68e5b6 | ||
|
|
0e254cbbef
|
||
|
|
7b94adafbd
|
||
|
|
61f39d1e09 | ||
|
|
e7739de51c | ||
|
|
ad1d240f20 | ||
|
|
8b0ef662bc
|
||
|
|
0a58c20ad7 | ||
|
|
38ddb29aa0 | ||
|
|
8b9a70c5a6 | ||
|
|
48a084914a | ||
|
|
a042b04357 | ||
|
|
35ca2afc6f | ||
|
|
b14f977e33 | ||
|
|
eef4500599 | ||
|
|
73af1451b7
|
||
|
|
36a3704815 | ||
|
|
359cb0a301 | ||
|
|
4b10e85053
|
||
|
|
c942a2cf2d | ||
|
|
f65afa6046 | ||
|
|
389d8e06e0
|
||
|
|
0008b55b70
|
||
|
|
d16ae9c745
|
||
|
|
36f94151b8 | ||
|
|
5326ad32f5 | ||
|
|
d199376364
|
||
|
|
3aee88c150
|
||
|
|
70da3ee8bd | ||
|
|
aa8eb753f6
|
||
|
|
8d73de8731
|
||
|
|
5a98397efe
|
||
|
|
a117c5759c | ||
|
|
ae7e6f82a8 | ||
|
|
1158be5b39 | ||
|
|
33e767458f | ||
|
|
94a65416ae | ||
|
|
0a384a22c9 | ||
|
|
b3b45521b6
|
||
|
|
131b23efa9
|
||
|
|
e2afceb492 | ||
|
|
7be1843c41 | ||
|
|
c09d009a3e
|
||
|
|
2007e28be8 | ||
|
|
d5bfdcae7b | ||
|
|
311f1e8ee5 | ||
|
|
e6a16a069b | ||
|
|
af67c53dd6 | ||
|
|
ea79e331fa
|
||
|
|
ee89b0c8a9 | ||
|
|
f2fd58cd2b | ||
|
|
1280a30216
|
||
|
|
a80ed72b2d
|
||
|
|
4cc6c12dc7 | ||
|
|
425004879a | ||
|
|
76f99e6518 | ||
|
|
f1e260e996 | ||
|
|
54e90754ef
|
||
|
|
487143802a | ||
|
|
e6a004ab8b | ||
|
|
b510c54875 | ||
|
|
e1ea464bc9 | ||
|
|
b46b8dfa41 | ||
|
|
ca067a6ccf | ||
|
|
d719b346e0 | ||
|
|
a1da3dcdc8
|
||
|
|
9927ef1581
|
||
|
|
791c993870
|
||
|
|
38dbce517c
|
||
|
|
889dc9c009
|
||
|
|
097021072d
|
||
|
|
91c8eb8faf
|
||
|
|
eed0a6a243 | ||
|
|
d33009d4a3 | ||
|
|
8d0535f3ca | ||
|
|
75f9b8a803
|
||
|
|
c30cce0a14
|
||
|
|
3e6591e390 | ||
|
|
f033f87329 | ||
|
|
29cb8c7fb4
|
||
|
|
1dcfed86ab | ||
|
|
efe50ed1e4 | ||
|
|
5b44981688 | ||
|
|
2add95d541
|
||
|
|
f62fff2585
|
||
|
|
11c196821d | ||
|
|
43ebc7d371
|
||
|
|
639e331f24 | ||
|
|
78be72e32f
|
||
|
|
3932e53ced | ||
|
|
097b619d71 | ||
|
|
f7abcedd75
|
||
|
|
807c0ff3db | ||
|
|
7e6f9672cf | ||
|
|
9fe13601fb
|
||
|
|
920cbab1bc | ||
|
|
17d97f0b7e | ||
|
|
10463e7348
|
||
|
|
e9abbd5f05
|
||
|
|
d6ff50455a
|
||
|
|
b1bdeabca8 | ||
|
|
da3c971ee6 | ||
|
|
c02edc90cc
|
||
|
|
4d1a20d69b
|
||
|
|
7e86c4ea3d | ||
|
|
c4f99fec2f | ||
|
|
c6328eef09
|
||
|
|
dc52bc2fba | ||
|
|
a54f03f0cd | ||
|
|
799cce6991 | ||
|
|
6b2cb002ac | ||
|
|
e84674e3b5 | ||
|
|
6ca5cede3e
|
||
|
|
4d010e6a18
|
||
|
|
5250ca8214 | ||
|
|
49f89e6452 | ||
|
|
89723e2ccb
|
||
|
|
d05e2bd8ec
|
||
|
|
7484d3c102
|
||
|
|
f78a875ba3
|
||
|
|
a025652542
|
||
|
|
91a01b86a9 | ||
|
|
105713361e | ||
|
|
4cb0dbfaad | ||
|
|
801cdcafca
|
||
|
|
094bcce0dc
|
||
|
|
d1ec678d7a
|
||
|
|
f0324cd93a
|
||
|
|
1b2ee03678
|
||
|
|
18e61d2048
|
||
|
|
f0a11c2c99
|
||
|
|
bc8d1edf6f
|
||
|
|
d48d880ba3 | ||
|
|
7c9b65db8b | ||
|
|
e5c1135501 | ||
|
|
430373f010 | ||
|
|
b68d17614d
|
||
|
|
30712738dc | ||
|
|
0915b23dc8 | ||
|
|
db30c61327 | ||
|
|
27f5b2bb58
|
||
|
|
baabdb6d30
|
||
|
|
3a67e23bc3
|
||
|
|
13e2b5f8c8
|
||
|
|
53aa58d044 | ||
|
|
d8934647a9 | ||
|
|
7ac51cd5e9
|
||
|
|
52bab1d611 | ||
|
|
49e46e6b9b
|
||
|
|
c1c40c8d40
|
||
|
|
c71482cb44
|
||
|
|
05cf4a6fe5 | ||
|
|
9b4de93283
|
||
|
|
16ffbbc4b3
|
||
|
|
de4f3efa30
|
||
|
|
bc7dde3b02 | ||
|
|
7a64488ed5
|
||
|
|
5f3c3871d3 | ||
|
|
4d24e22bb5 | ||
|
|
c47cfb52af | ||
|
|
da0087bba6
|
||
|
|
8338f27794
|
||
|
|
b029d65c90
|
||
|
|
c24f99899b
|
||
|
|
3aca581764
|
||
|
|
ba540d09b2
|
||
|
|
6530d2ccbc
|
||
|
|
a784091ecb
|
||
|
|
61c3e1e3c6 | ||
|
|
ce76a75630
|
||
|
|
52249db5b4
|
||
|
|
09d8b52fbf
|
||
|
|
0edd566904
|
||
|
|
6eb1b0f197
|
||
|
|
e4137d9760
|
||
|
|
864f4124ae
|
||
|
|
7514985feb | ||
|
|
d6c72806bb | ||
|
|
3502cdc607
|
||
|
|
d51e7fe401 | ||
|
|
f9a4039ad2
|
||
|
|
8e5c21b443
|
||
|
|
55b350c3a2
|
||
|
|
54324df3be
|
||
|
|
35adf8299c | ||
|
|
2d4f2d1139
|
||
|
|
77e632276b
|
||
|
|
4c95b57885
|
||
|
|
242402b253
|
||
|
|
61d15f9431
|
||
|
|
508864bcbb
|
||
|
|
23c54bb01e
|
||
|
|
ec667c64e8
|
||
|
|
39b2ccad8e
|
||
|
|
23815945bf
|
||
|
|
9dca83acd9
|
||
|
|
55300e2d8c
|
||
|
|
28afd15134
|
||
|
|
58304757aa
|
||
|
|
c95518e94a
|
||
|
|
5ee4617607 | ||
|
|
842008b089
|
||
|
|
6f56a0bcf6
|
||
|
|
5feed360ca | ||
|
|
c17f0a4080 | ||
|
|
0317c7f011 | ||
|
|
13797b5005
|
||
|
|
b24d9d7487
|
||
|
|
3a01cffc6b | ||
|
|
eddf6f0456
|
||
|
|
f6c024d61e
|
||
|
|
6749ff843c | ||
|
|
42abdd1268
|
||
|
|
5d914b1547
|
||
|
|
50b45cac0b | ||
|
|
e35aac6ee0
|
||
|
|
fe2da22d29
|
||
|
|
2045ffbff8
|
||
|
|
478869ff28 | ||
|
|
9eed37420e | ||
|
|
99f4d2baaf | ||
|
|
f4e8c3feec
|
||
|
|
d0b308f340
|
||
|
|
1b56360a24 | ||
|
|
68833c76c4
|
||
|
|
4d7c80f2e4 | ||
|
|
2f17859b7b
|
||
|
|
9c7e02cbf0
|
||
|
|
5f320edab5
|
||
|
|
f7ce3371a1
|
||
|
|
71805dccd7
|
||
|
|
a13b7352d7
|
||
|
|
ff5ecdaded
|
||
|
|
b32c3cf58c
|
||
|
|
b791262860
|
||
|
|
29b8c6e02a
|
||
|
|
618727d8e8
|
||
|
|
fed60c265d
|
||
|
|
a34a7489db
|
||
|
|
e59192bbe1
|
||
|
|
e0f82d28f0
|
||
|
|
a0521aeeaf
|
||
|
|
2127f759ca
|
||
|
|
5e787183d0
|
||
|
|
81ca31b899
|
||
|
|
e2a7597b4f
|
||
|
|
2e59c21078
|
||
|
|
7b5ab3294d
|
||
|
|
2bbf38f987
|
||
|
|
f09c91494d
|
||
|
|
58ec9b76e0
|
||
|
|
7a196f69d6
|
||
|
|
c799a8de3c | ||
|
|
34d2dce8dc
|
||
|
|
3a22a97761
|
||
|
|
962243e959
|
||
|
|
021010a338
|
||
|
|
4c0575afe0
|
||
|
|
9e46176519
|
||
|
|
f10e905dbd
|
||
|
|
e4aa8ff907 | ||
|
|
a6ece5388a
|
||
|
|
6a44b54b51
|
||
|
|
93cd688625
|
||
|
|
8e319a417d
|
||
|
|
38034db1e4
|
||
|
|
f775f90360
|
||
|
|
55dff6ced7
|
||
|
|
d0c11d347b
|
||
|
|
f0418c6e56
|
||
|
|
e18985fb14 | ||
|
|
2f07c3407a
|
||
|
|
a5554ec530
|
||
|
|
f9f2fe6e87
|
||
|
|
8ca05859a9
|
||
|
|
0cac446725
|
||
|
|
23623ad1e1
|
||
|
|
b623c5e160
|
||
|
|
5436e0cd49
|
||
|
|
beeeee5ebd
|
||
|
|
fdbf769760
|
||
|
|
0a36d1aa99
|
||
|
|
69ab87c25f
|
||
|
|
9a30419a23
|
||
|
|
092c56f98f
|
||
|
|
10ef535f9a
|
||
|
|
6c80bd5843
|
||
|
|
f0bd0ba355
|
||
|
|
be4db24861
|
||
|
|
83d21c4b6d
|
||
|
|
e744fab067
|
||
|
|
5167e3a494
|
||
|
|
aff4e91bbb
|
||
|
|
737101fe9e
|
||
|
|
629fe97ef7
|
||
|
|
fa97472bce
|
||
|
|
83f13df627
|
||
|
|
cde231b1ff
|
||
|
|
7161fc3513
|
||
|
|
9a91951656
|
||
|
|
11e9c721c6
|
||
|
|
3c66ea6b30
|
||
|
|
79f37f3986
|
||
|
|
f1b85b0751
|
No files matched your search
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: subminer-change-verification
|
||||||
|
description: Verify SubMiner changes with repo-native cheap-first test lanes. Use after code, config, launcher, plugin, runtime, stats, documentation, or workflow changes; do not use for read-only questions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# SubMiner Change Verification
|
||||||
|
|
||||||
|
Verify the behavior claimed by a change without running unrelated expensive checks by default.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Inspect the requested scope and changed paths with `git status --short` and `git diff`.
|
||||||
|
2. Read `docs/workflow/verification.md` as the source of truth for maintained lanes.
|
||||||
|
3. Run the cheapest lane or lanes that cover the changed behavior.
|
||||||
|
4. Escalate to the full handoff gate only for substantial or cross-boundary changes.
|
||||||
|
5. Report exact commands, results, skipped checks, blockers, and remaining risk.
|
||||||
|
|
||||||
|
Do not use hidden wrapper commands. Verification commands are owned by `package.json` and the workflow documentation.
|
||||||
|
|
||||||
|
## Lane Selection
|
||||||
|
|
||||||
|
- Internal docs, `AGENTS.md`, or `.agents/skills/**`: `bun run test:docs:kb`
|
||||||
|
- User-facing `docs-site/**`: `bun run docs:test`, then `bun run docs:build`
|
||||||
|
- Config/schema/defaults: `bun run test:config`
|
||||||
|
- If defaults or templates changed, also run `bun run generate:config-example` and `bun run verify:config-example`.
|
||||||
|
- General TypeScript source: `bun run typecheck`, then `bun run test:fast`
|
||||||
|
- Launcher or mpv plugin: `bun run test:launcher` or `bun run test:env`, based on the behavior changed
|
||||||
|
- Runtime compatibility or dist-sensitive wiring: `bun run test:runtime:compat`
|
||||||
|
- Stats dashboard: `bun run test:stats`
|
||||||
|
- Build/release scripts: `bun run test:scripts`
|
||||||
|
|
||||||
|
For substantial changes, use the full gate documented in `AGENTS.md` and `docs/workflow/verification.md`.
|
||||||
|
|
||||||
|
## Runtime Escalation
|
||||||
|
|
||||||
|
Real runtime checks are required when the claim depends on actual Electron, mpv, overlay, focus, window tracking, launch, or socket behavior. Run the relevant application flow when the environment supports it. Otherwise, report the missing runtime dependency and do not present cheaper checks as authoritative runtime validation.
|
||||||
|
|
||||||
|
## Pre-Handoff Checks
|
||||||
|
|
||||||
|
Before handoff, reconcile both questions:
|
||||||
|
|
||||||
|
1. Do behavior, defaults, flags, shortcuts, ports, APIs, architecture, or workflow changes require documentation updates?
|
||||||
|
2. Does the change require a current-outcome fragment under `changes/` according to `changes/README.md`?
|
||||||
|
|
||||||
|
Complete required updates before handoff or report the blocker.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: subminer-release
|
||||||
|
description: Prepare, cut, publish, or repair SubMiner stable and prerelease releases. Use for hands-on release work; do not use for general release questions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# SubMiner release
|
||||||
|
|
||||||
|
Carry out the requested release phase using the repository's current release process.
|
||||||
|
|
||||||
|
## Source of truth
|
||||||
|
|
||||||
|
Read `docs/RELEASING.md` completely before changing files or release state. Treat it as canonical. Read `changes/README.md` when the work touches change fragments or generated release notes.
|
||||||
|
|
||||||
|
Do not copy release commands or policy into this skill. If this skill disagrees with the release guide, follow the guide and reconcile the skill before handoff.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Identify whether the request is for a stable release, prerelease, release preparation, publication, or repair.
|
||||||
|
2. Inspect the current branch, worktree status, package version, pending change fragments, relevant tags, and latest CI state before making changes.
|
||||||
|
3. Follow the matching procedure in `docs/RELEASING.md` in order. Review generated changelog and release-note Markdown before it can be committed or published.
|
||||||
|
4. Run every required gate for the requested release phase. Do not treat a cheaper test lane as a substitute for the documented release gate.
|
||||||
|
5. Before a stable tag, confirm the package and tag versions match and no pending `changes/*.md` fragments remain. Preserve fragments for prereleases as documented.
|
||||||
|
6. Report the resulting version, completed checks, local commit and tag state, remote publication state, skipped platform checks, and any remaining manual work.
|
||||||
|
|
||||||
|
## Authorization boundaries
|
||||||
|
|
||||||
|
- A request to prepare a release stops before commit, tag, push, or remote publication unless the user also authorizes those actions.
|
||||||
|
- A clear request to cut or publish a release includes the documented commit, tag, and push steps. Ask before the first remote mutation when the wording is ambiguous.
|
||||||
|
- Do not edit an existing GitHub release, publish to the AUR, change secrets, or alter signing configuration unless the user explicitly requests that operation.
|
||||||
|
- Do not switch branches without consent.
|
||||||
|
|
||||||
|
## Stop conditions
|
||||||
|
|
||||||
|
Stop and report the blocker when required CI or a release gate fails, authentication is missing, versions disagree, required artifacts are absent, or the worktree contains unexpected changes that overlap the release. Do not tag or publish a partially verified release.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [ksyasuda]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: sudacode
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report something that is broken or behaving incorrectly
|
||||||
|
title: '[Bug]: '
|
||||||
|
labels: ['bug']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to file a bug report! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: A clear description of the bug, including what you expected to happen instead.
|
||||||
|
placeholder: When I open the Yomitan popup, the overlay freezes...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Minimal, ordered steps that reliably trigger the bug.
|
||||||
|
placeholder: |
|
||||||
|
1. Launch `subminer`
|
||||||
|
2. Play a video in MPV
|
||||||
|
3. Hover a word and press ...
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Affected area
|
||||||
|
description: Which part of SubMiner is affected?
|
||||||
|
options:
|
||||||
|
- Overlay / Yomitan popup
|
||||||
|
- Anki mining
|
||||||
|
- Subtitle annotations
|
||||||
|
- Subtitle sidebar
|
||||||
|
- Immersion tracking / stats
|
||||||
|
- Launcher / CLI
|
||||||
|
- MPV plugin
|
||||||
|
- Jellyfin integration
|
||||||
|
- Jimaku integration
|
||||||
|
- AniList integration
|
||||||
|
- YouTube integration
|
||||||
|
- Character dictionary
|
||||||
|
- WebSocket / texthooker API
|
||||||
|
- Configuration
|
||||||
|
- Documentation
|
||||||
|
- Other / not sure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: SubMiner version
|
||||||
|
description: Output of `subminer --version`, or the release tag / commit you are running.
|
||||||
|
placeholder: v0.15.0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor (Linux only)
|
||||||
|
description: SubMiner's overlay supports Hyprland and sway. Name yours (and version if known). Leave blank on macOS / Windows.
|
||||||
|
placeholder: Hyprland 0.55
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: mpv-version
|
||||||
|
attributes:
|
||||||
|
label: MPV version
|
||||||
|
description: Output of `mpv --version` (first line).
|
||||||
|
placeholder: mpv 0.38.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs / console output
|
||||||
|
description: |
|
||||||
|
Relevant logs. For verbose output, run `electron . --dev --log-level debug`.
|
||||||
|
This will be rendered as code automatically — no backticks needed.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Documentation
|
||||||
|
url: https://docs.subminer.moe
|
||||||
|
about: Setup, configuration, and feature docs — check here before filing an issue.
|
||||||
|
- name: Troubleshooting guide
|
||||||
|
url: https://docs.subminer.moe/troubleshooting
|
||||||
|
about: Common problems and fixes (Hyprland rules, MPV detection, Anki connection, etc.).
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or an improvement to an existing one
|
||||||
|
title: '[Feature]: '
|
||||||
|
labels: ['enhancement']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for the idea! Please search [existing issues](https://github.com/ksyasuda/SubMiner/issues?q=is%3Aissue) first to avoid duplicates.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem / motivation
|
||||||
|
description: What problem are you trying to solve? What is missing or frustrating today?
|
||||||
|
placeholder: When mining a card I have to manually switch to Anki because...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposal
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
description: Describe the feature or change you'd like to see.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: area
|
||||||
|
attributes:
|
||||||
|
label: Related area
|
||||||
|
description: Which part of SubMiner does this relate to?
|
||||||
|
options:
|
||||||
|
- Overlay / Yomitan popup
|
||||||
|
- Anki mining
|
||||||
|
- Subtitle annotations
|
||||||
|
- Subtitle sidebar
|
||||||
|
- Immersion tracking / stats
|
||||||
|
- Launcher / CLI
|
||||||
|
- MPV plugin
|
||||||
|
- Jellyfin integration
|
||||||
|
- Jimaku integration
|
||||||
|
- AniList integration
|
||||||
|
- YouTube integration
|
||||||
|
- Character dictionary
|
||||||
|
- WebSocket / texthooker API
|
||||||
|
- Configuration
|
||||||
|
- Documentation
|
||||||
|
- Other / new area
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: Any workarounds you currently use or other approaches you've thought about.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
Thanks for contributing to SubMiner! Fill out the sections below.
|
||||||
|
Keep it short — a couple of sentences per section is fine.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- What does this PR do and why? -->
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
<!-- Check all that apply. -->
|
||||||
|
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature
|
||||||
|
- [ ] Refactor / internal
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
## Related issues
|
||||||
|
|
||||||
|
<!-- e.g. "Closes #123". Delete if none. -->
|
||||||
|
|
||||||
|
## How was this tested?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe verification. The default handoff gate is:
|
||||||
|
bun run typecheck && bun run test:fast && bun run test:env && bun run build && bun run test:smoke:dist
|
||||||
|
If docs-site/ changed, also: bun run docs:test && bun run docs:build
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Reconciled current-outcome changelog fragment(s), or this PR is labeled `skip-changelog` (see [`changes/README.md`](../changes/README.md))
|
||||||
|
- [ ] Docs updated in the same PR if behavior, defaults, flags, shortcuts, ports, or APIs changed
|
||||||
|
- [ ] Relevant checks pass locally (typecheck, tests, build)
|
||||||
@@ -8,67 +8,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-test-audit:
|
build-test-audit:
|
||||||
runs-on: ubuntu-latest
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build (TypeScript check)
|
|
||||||
# Keep explicit typecheck for fast fail before full build/bundle.
|
|
||||||
run: bun run tsc --noEmit
|
|
||||||
|
|
||||||
- name: Test suite (source)
|
|
||||||
run: bun run test:fast
|
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
|
||||||
run: bun run test:launcher:smoke:src
|
|
||||||
|
|
||||||
- name: Upload launcher smoke artifacts (on failure)
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: launcher-smoke
|
|
||||||
path: .tmp/launcher-smoke/**
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Build (bundle)
|
|
||||||
run: bun run build
|
|
||||||
|
|
||||||
- name: Dist smoke suite
|
|
||||||
run: bun run test:smoke:dist
|
|
||||||
|
|
||||||
- name: Build docs
|
|
||||||
run: bun run docs:build
|
|
||||||
|
|
||||||
- name: Security audit
|
|
||||||
run: bun audit --audit-level high
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Build Bun subminer wrapper
|
|
||||||
run: make build-launcher
|
|
||||||
|
|
||||||
- name: Verify Bun subminer wrapper
|
|
||||||
run: dist/launcher/subminer --help >/dev/null
|
|
||||||
|
|
||||||
- name: Enforce generated launcher workflow
|
|
||||||
run: bash scripts/verify-generated-launcher.sh
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: Docs Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
rebuild_archives:
|
||||||
|
description: 'Stable tags to rebuild in R2 (comma-separated, or "all"); archives are otherwise built once'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
paths:
|
||||||
|
- 'docs-site/**'
|
||||||
|
- 'scripts/docs-versioning.ts'
|
||||||
|
- 'scripts/docs-versioned-assets.ts'
|
||||||
|
- 'scripts/docs-archive-store.ts'
|
||||||
|
- 'scripts/build-versioned-docs.ts'
|
||||||
|
- '.github/workflows/docs-pages.yml'
|
||||||
|
- 'package.json'
|
||||||
|
- 'bun.lock'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: docs-pages-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
if: ${{ github.ref_type != 'tag' || !contains(github.ref_name, '-') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Guard stable docs tag shape
|
||||||
|
id: tag_guard
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::notice::Skipping non-stable docs tag $TAG_NAME"
|
||||||
|
echo "stable_tag=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "stable_tag=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd docs-site && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Test docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
run: bun run docs:test
|
||||||
|
|
||||||
|
# Builds only archives missing from R2 (plus any requested rebuilds), then the
|
||||||
|
# root and /main/ builds that make up the Pages deployment.
|
||||||
|
- name: Build versioned docs
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
DOCS_ARCHIVE_R2_ACCESS_KEY_ID: ${{ secrets.DOCS_ARCHIVE_R2_ACCESS_KEY_ID }}
|
||||||
|
DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY: ${{ secrets.DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY }}
|
||||||
|
DOCS_ARCHIVE_R2_BUCKET: ${{ vars.DOCS_ARCHIVE_R2_BUCKET }}
|
||||||
|
REBUILD_ARCHIVES: ${{ inputs.rebuild_archives }}
|
||||||
|
run: bun run scripts/build-versioned-docs.ts --require-archives "--rebuild-archives=${REBUILD_ARCHIVES}"
|
||||||
|
|
||||||
|
- name: Deploy docs to Cloudflare Pages
|
||||||
|
if: steps.tag_guard.outputs.stable_tag != 'false'
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
# Run from docs-site so Wrangler bundles docs-site/functions (the /v/* archive server).
|
||||||
|
workingDirectory: docs-site
|
||||||
|
command: pages deploy ../.tmp/docs-versioned-site --project-name "${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }}" --branch main
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
name: Package release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
CSC_LINK:
|
||||||
|
required: true
|
||||||
|
CSC_KEY_PASSWORD:
|
||||||
|
required: true
|
||||||
|
APPLE_ID:
|
||||||
|
required: true
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD:
|
||||||
|
required: true
|
||||||
|
APPLE_TEAM_ID:
|
||||||
|
required: true
|
||||||
|
# Project TMDB key baked into release artifacts; builds stay valid without it.
|
||||||
|
SUBMINER_TMDB_API_KEY:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
run: |
|
||||||
|
cd vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Build AppImage
|
||||||
|
run: bun run build:appimage
|
||||||
|
env:
|
||||||
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
|
|
||||||
|
- name: Build unversioned AppImage
|
||||||
|
run: |
|
||||||
|
shopt -s nullglob
|
||||||
|
appimages=(release/SubMiner-*.AppImage)
|
||||||
|
if [ "${#appimages[@]}" -eq 0 ]; then
|
||||||
|
echo "No versioned AppImage found to create unversioned artifact."
|
||||||
|
ls -la release
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp "${appimages[0]}" release/SubMiner.AppImage
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: xvfb-run -a bun run test:package "release/linux-unpacked/resources"
|
||||||
|
|
||||||
|
- name: Upload AppImage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: appimage
|
||||||
|
path: |
|
||||||
|
release/*.AppImage
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Validate macOS signing/notarization secrets
|
||||||
|
run: |
|
||||||
|
missing=0
|
||||||
|
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
||||||
|
if [ -z "${!name}" ]; then
|
||||||
|
echo "Missing required secret: $name"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$missing" -ne 0 ]; then
|
||||||
|
echo "Set all required macOS signing/notarization secrets and rerun."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
run: |
|
||||||
|
cd vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Build signed + notarized macOS artifacts
|
||||||
|
run: bun run build:mac
|
||||||
|
env:
|
||||||
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: bun run test:package "release/mac-arm64/SubMiner.app/Contents/Resources"
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos
|
||||||
|
path: |
|
||||||
|
release/*.dmg
|
||||||
|
release/*.zip
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/texthooker-ui/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build texthooker-ui
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Set-Location vendor/texthooker-ui
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
- name: Verify managed Windows launcher
|
||||||
|
run: bun test src/main/runtime/managed-launcher.test.ts
|
||||||
|
|
||||||
|
- name: Verify Windows launcher bootstrap
|
||||||
|
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
|
||||||
|
|
||||||
|
- name: Build unsigned Windows artifacts
|
||||||
|
run: bun run build:win:unsigned
|
||||||
|
env:
|
||||||
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
|
|
||||||
|
- name: Smoke packaged runtime assets
|
||||||
|
shell: bash
|
||||||
|
run: bun run test:package "release/win-unpacked/resources"
|
||||||
|
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows
|
||||||
|
path: |
|
||||||
|
release/*.exe
|
||||||
|
release/*.zip
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
name: Prerelease
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*-beta.*'
|
||||||
|
- 'v*-rc.*'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: prerelease-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality-gate:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
|
|
||||||
|
package:
|
||||||
|
needs: [quality-gate]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/package-release.yml
|
||||||
|
secrets:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [package]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download AppImage
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: appimage
|
||||||
|
path: release
|
||||||
|
|
||||||
|
- name: Download macOS artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos
|
||||||
|
path: release
|
||||||
|
|
||||||
|
- name: Download Windows artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows
|
||||||
|
path: release
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build launcher runtime artifacts
|
||||||
|
run: make build-launcher
|
||||||
|
|
||||||
|
- name: Smoke launcher bundle
|
||||||
|
run: bun dist/launcher/subminer.js --help >/dev/null
|
||||||
|
|
||||||
|
- name: Enforce generated launcher workflow
|
||||||
|
run: bash scripts/verify-generated-launcher.sh
|
||||||
|
|
||||||
|
- name: Verify generated config examples
|
||||||
|
run: bun run verify:config-example
|
||||||
|
|
||||||
|
- name: Package optional assets bundle
|
||||||
|
run: |
|
||||||
|
tar -czf "release/subminer-assets.tar.gz" \
|
||||||
|
config.example.jsonc \
|
||||||
|
plugin/subminer \
|
||||||
|
plugin/subminer.conf \
|
||||||
|
assets/themes/subminer.rasi \
|
||||||
|
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
|
||||||
|
resources/bun/licenses
|
||||||
|
|
||||||
|
- name: Package Bun corresponding source
|
||||||
|
run: bun scripts/package-bun-source.mjs
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
shopt -s nullglob
|
||||||
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||||
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
|
echo "No release artifacts found for checksum generation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
: > release/SHA256SUMS.txt
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
printf '%s %s\n' \
|
||||||
|
"$(sha256sum "$file" | awk '{print $1}')" \
|
||||||
|
"${file##*/}" >> release/SHA256SUMS.txt
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Verify committed prerelease notes
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
if [ ! -s release/prerelease-notes.md ]; then
|
||||||
|
echo "::error::release/prerelease-notes.md is missing or empty. Run 'bun run changelog:prerelease-notes --version <version>' locally and commit the file before tagging."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! bun run changelog:check-prerelease-notes --version "$RELEASE_VERSION"; then
|
||||||
|
echo "::error::release/prerelease-notes.md was not generated for $RELEASE_VERSION. Rerun 'bun run changelog:prerelease-notes --version $RELEASE_VERSION' locally, commit, and retag."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish Prerelease
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
artifacts=(
|
||||||
|
release/*.AppImage
|
||||||
|
release/*.dmg
|
||||||
|
release/*.exe
|
||||||
|
release/*.zip
|
||||||
|
release/*.tar.gz
|
||||||
|
release/*.tar.gz.sha256
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
|
release/SHA256SUMS.txt
|
||||||
|
dist/launcher/subminer
|
||||||
|
dist/launcher/subminer.cmd
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#artifacts[@]}" -eq 0 ]; then
|
||||||
|
echo "No release artifacts found for upload."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
|
||||||
|
gh release edit "$RELEASE_VERSION" \
|
||||||
|
--draft \
|
||||||
|
--prerelease \
|
||||||
|
--title "$RELEASE_VERSION" \
|
||||||
|
--notes-file release/prerelease-notes.md
|
||||||
|
else
|
||||||
|
gh release create "$RELEASE_VERSION" \
|
||||||
|
--draft \
|
||||||
|
--latest=false \
|
||||||
|
--prerelease \
|
||||||
|
--title "$RELEASE_VERSION" \
|
||||||
|
--notes-file release/prerelease-notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
for asset in "${artifacts[@]}"; do
|
||||||
|
gh release upload "$RELEASE_VERSION" "$asset" --clobber
|
||||||
|
done
|
||||||
|
|
||||||
|
gh release edit "$RELEASE_VERSION" \
|
||||||
|
--draft=false \
|
||||||
|
--prerelease \
|
||||||
|
--title "$RELEASE_VERSION" \
|
||||||
|
--notes-file release/prerelease-notes.md
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
name: Quality Gate
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
launcher-runtime:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Verify native runtime staging
|
||||||
|
run: bun test src/main/runtime/managed-launcher.test.ts
|
||||||
|
|
||||||
|
- name: Verify Windows launcher bootstrap
|
||||||
|
run: bun test src/main/runtime/windows-launcher-bootstrap.test.ts
|
||||||
|
|
||||||
|
- name: Verify native mpv process launch
|
||||||
|
run: bun test src/main/runtime/mpv-process.test.ts
|
||||||
|
|
||||||
|
- name: Verify POSIX launcher bootstrap
|
||||||
|
run: bun test src/main/runtime/posix-launcher-bootstrap.test.ts
|
||||||
|
|
||||||
|
quality-gate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: true
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.5
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
stats/node_modules
|
||||||
|
vendor/subminer-yomitan/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ runner.arch }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install --frozen-lockfile
|
||||||
|
cd stats && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint changelog fragments
|
||||||
|
run: bun run changelog:lint
|
||||||
|
|
||||||
|
- name: Lint stats (formatting)
|
||||||
|
run: bun run lint:stats
|
||||||
|
|
||||||
|
- name: Enforce pull request changelog fragments (`skip-changelog` label bypass)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
|
||||||
|
run: bun run changelog:pr-check --base-ref "origin/$BASE_REF" --head-ref "HEAD" --labels "$PR_LABELS"
|
||||||
|
|
||||||
|
- name: Build (TypeScript check)
|
||||||
|
run: bun run typecheck
|
||||||
|
|
||||||
|
- name: Verify generated config examples
|
||||||
|
run: bun run verify:config-example
|
||||||
|
|
||||||
|
- name: Install Lua
|
||||||
|
run: |
|
||||||
|
# Lua needs only Ubuntu sources; unrelated runner repositories can be unavailable.
|
||||||
|
test -f /etc/apt/sources.list.d/ubuntu.sources
|
||||||
|
apt_sources=(-o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources -o Dir::Etc::sourceparts=-)
|
||||||
|
sudo apt-get "${apt_sources[@]}" update
|
||||||
|
sudo apt-get "${apt_sources[@]}" install -y lua5.4
|
||||||
|
sudo ln -sf /usr/bin/lua5.4 /usr/local/bin/lua
|
||||||
|
lua -v
|
||||||
|
|
||||||
|
- name: Launcher unit and script suites
|
||||||
|
run: bun run test:launcher:unit:src && bun run test:scripts
|
||||||
|
|
||||||
|
- name: Environment suite
|
||||||
|
run: bun run test:env
|
||||||
|
|
||||||
|
- name: Upload launcher smoke artifacts (on failure)
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: launcher-smoke
|
||||||
|
path: .tmp/launcher-smoke/**
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
- name: Coverage suite (maintained source lane)
|
||||||
|
run: bun run test:coverage:src
|
||||||
|
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-test-src
|
||||||
|
path: coverage/test-src/lcov.info
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Stats UI tests
|
||||||
|
run: bun run test:stats
|
||||||
|
|
||||||
|
- name: Build (bundle)
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Immersion SQLite verification
|
||||||
|
run: bun run test:immersion:sqlite:dist
|
||||||
|
|
||||||
|
- name: Dist smoke suite
|
||||||
|
run: bun run test:smoke:dist
|
||||||
|
|
||||||
|
- name: Security audit
|
||||||
|
run: bun audit --audit-level high
|
||||||
|
|
||||||
|
- name: Build launcher runtime artifacts
|
||||||
|
run: make build-launcher
|
||||||
|
|
||||||
|
- name: Smoke launcher bundle
|
||||||
|
run: bun dist/launcher/subminer.js --help >/dev/null
|
||||||
|
|
||||||
|
- name: Enforce generated launcher workflow
|
||||||
|
run: bash scripts/verify-generated-launcher.sh
|
||||||
@@ -4,195 +4,37 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
- '!v*-beta.*'
|
||||||
|
- '!v*-rc.*'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality-gate:
|
quality-gate:
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/quality-gate.yml
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
package:
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Test suite (source)
|
|
||||||
run: bun run test:fast
|
|
||||||
|
|
||||||
- name: Launcher smoke suite (source)
|
|
||||||
run: bun run test:launcher:smoke:src
|
|
||||||
|
|
||||||
- name: Upload launcher smoke artifacts (on failure)
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: launcher-smoke
|
|
||||||
path: .tmp/launcher-smoke/**
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Build (bundle)
|
|
||||||
run: bun run build
|
|
||||||
|
|
||||||
- name: Dist smoke suite
|
|
||||||
run: bun run test:smoke:dist
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
needs: [quality-gate]
|
needs: [quality-gate]
|
||||||
runs-on: ubuntu-latest
|
permissions:
|
||||||
steps:
|
contents: read
|
||||||
- name: Checkout
|
uses: ./.github/workflows/package-release.yml
|
||||||
uses: actions/checkout@v4
|
secrets:
|
||||||
with:
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
submodules: true
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
- name: Setup Bun
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
uses: oven-sh/setup-bun@v2
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
with:
|
SUBMINER_TMDB_API_KEY: ${{ secrets.SUBMINER_TMDB_API_KEY }}
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build AppImage
|
|
||||||
run: bun run build:appimage
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build unversioned AppImage
|
|
||||||
run: |
|
|
||||||
shopt -s nullglob
|
|
||||||
appimages=(release/SubMiner-*.AppImage)
|
|
||||||
if [ "${#appimages[@]}" -eq 0 ]; then
|
|
||||||
echo "No versioned AppImage found to create unversioned artifact."
|
|
||||||
ls -la release
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
cp "${appimages[0]}" release/SubMiner.AppImage
|
|
||||||
|
|
||||||
- name: Upload AppImage artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: appimage
|
|
||||||
path: release/*.AppImage
|
|
||||||
|
|
||||||
build-macos:
|
|
||||||
needs: [quality-gate]
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.3.5
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.bun/install/cache
|
|
||||||
node_modules
|
|
||||||
vendor/texthooker-ui/node_modules
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'vendor/texthooker-ui/package.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Validate macOS signing/notarization secrets
|
|
||||||
run: |
|
|
||||||
missing=0
|
|
||||||
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
|
|
||||||
if [ -z "${!name}" ]; then
|
|
||||||
echo "Missing required secret: $name"
|
|
||||||
missing=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$missing" -ne 0 ]; then
|
|
||||||
echo "Set all required macOS signing/notarization secrets and rerun."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Build texthooker-ui
|
|
||||||
run: |
|
|
||||||
cd vendor/texthooker-ui
|
|
||||||
bun install
|
|
||||||
bun run build
|
|
||||||
|
|
||||||
- name: Build signed + notarized macOS artifacts
|
|
||||||
run: bun run build:mac
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
|
|
||||||
- name: Upload macOS artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: macos
|
|
||||||
path: |
|
|
||||||
release/*.dmg
|
|
||||||
release/*.zip
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-linux, build-macos]
|
needs: [package]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -211,6 +53,12 @@ jobs:
|
|||||||
name: macos
|
name: macos
|
||||||
path: release
|
path: release
|
||||||
|
|
||||||
|
- name: Download Windows artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows
|
||||||
|
path: release
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
@@ -229,94 +77,248 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Bun subminer wrapper
|
- name: Build launcher runtime artifacts
|
||||||
run: make build-launcher
|
run: make build-launcher
|
||||||
|
|
||||||
- name: Verify Bun subminer wrapper
|
- name: Smoke launcher bundle
|
||||||
run: dist/launcher/subminer --help >/dev/null
|
run: bun dist/launcher/subminer.js --help >/dev/null
|
||||||
|
|
||||||
- name: Enforce generated launcher workflow
|
- name: Enforce generated launcher workflow
|
||||||
run: bash scripts/verify-generated-launcher.sh
|
run: bash scripts/verify-generated-launcher.sh
|
||||||
|
|
||||||
|
- name: Verify generated config examples
|
||||||
|
run: bun run verify:config-example
|
||||||
|
|
||||||
- name: Package optional assets bundle
|
- name: Package optional assets bundle
|
||||||
run: |
|
run: |
|
||||||
tar -czf "release/subminer-assets.tar.gz" \
|
tar -czf "release/subminer-assets.tar.gz" \
|
||||||
config.example.jsonc \
|
config.example.jsonc \
|
||||||
plugin/subminer \
|
plugin/subminer \
|
||||||
plugin/subminer.conf \
|
plugin/subminer.conf \
|
||||||
assets/themes/subminer.rasi
|
assets/themes/subminer.rasi \
|
||||||
|
assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer \
|
||||||
|
resources/bun/licenses
|
||||||
|
|
||||||
|
- name: Package Bun corresponding source
|
||||||
|
run: bun scripts/package-bun-source.mjs
|
||||||
|
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: |
|
run: |
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
files=(release/*.AppImage release/*.dmg release/*.zip release/*.tar.gz dist/launcher/subminer)
|
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
|
||||||
if [ "${#files[@]}" -eq 0 ]; then
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
echo "No release artifacts found for checksum generation."
|
echo "No release artifacts found for checksum generation."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sha256sum "${files[@]}" > release/SHA256SUMS.txt
|
: > release/SHA256SUMS.txt
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
printf '%s %s\n' \
|
||||||
|
"$(sha256sum "$file" | awk '{print $1}')" \
|
||||||
|
"${file##*/}" >> release/SHA256SUMS.txt
|
||||||
|
done
|
||||||
|
|
||||||
- name: Get version from tag
|
- name: Get version from tag
|
||||||
id: version
|
id: version
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Generate changelog
|
- name: Guard against pending changelog fragments
|
||||||
id: changelog
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
run: |
|
run: |
|
||||||
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then
|
||||||
if [ -n "$PREV_TAG" ]; then
|
echo "::error::Pending changelog fragments detected. Run 'bun run changelog:build --version $RELEASE_VERSION' locally and commit the polished CHANGELOG.md before tagging. CI no longer auto-builds the changelog because the polish step requires the local 'claude' CLI."
|
||||||
CHANGES=$(git log --pretty=format:"- %s" ${PREV_TAG}..HEAD)
|
exit 1
|
||||||
else
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
if [ "$COMMIT_COUNT" -gt 10 ]; then
|
|
||||||
CHANGES=$(git log --pretty=format:"- %s" HEAD~10..HEAD)
|
|
||||||
else
|
|
||||||
CHANGES=$(git log --pretty=format:"- %s")
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
echo "CHANGES<<EOF" >> $GITHUB_OUTPUT
|
|
||||||
echo "$CHANGES" >> $GITHUB_OUTPUT
|
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create Release
|
- name: Verify changelog is ready for tagged release
|
||||||
uses: softprops/action-gh-release@v2
|
env:
|
||||||
with:
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
name: ${{ steps.version.outputs.VERSION }}
|
run: bun run changelog:check --version "$RELEASE_VERSION"
|
||||||
body: |
|
|
||||||
## Changes
|
|
||||||
${{ steps.changelog.outputs.CHANGES }}
|
|
||||||
|
|
||||||
## Installation
|
- name: Generate release notes from changelog
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: bun run changelog:release-notes --version "$RELEASE_VERSION"
|
||||||
|
|
||||||
### AppImage (Recommended)
|
- name: Publish Release
|
||||||
1. Download the AppImage below
|
env:
|
||||||
2. Make it executable: `chmod +x SubMiner.AppImage`
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
3. Run: `./SubMiner.AppImage`
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
### macOS
|
if gh release view "$RELEASE_VERSION" >/dev/null 2>&1; then
|
||||||
1. Download `subminer-*.dmg`
|
# Do not pass the prerelease flag here; gh defaults to a normal release.
|
||||||
2. Open the DMG and drag `SubMiner.app` into `/Applications`
|
gh release edit "$RELEASE_VERSION" \
|
||||||
3. If needed, use the ZIP artifact as an alternative
|
--draft=false \
|
||||||
|
--title "$RELEASE_VERSION" \
|
||||||
|
--notes-file release/release-notes.md
|
||||||
|
else
|
||||||
|
gh release create "$RELEASE_VERSION" \
|
||||||
|
--title "$RELEASE_VERSION" \
|
||||||
|
--notes-file release/release-notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
### Manual Installation
|
shopt -s nullglob
|
||||||
See the [README](https://github.com/${{ github.repository }}#installation) for manual installation instructions.
|
artifacts=(
|
||||||
|
|
||||||
### Optional Assets (config example + mpv plugin + rofi theme)
|
|
||||||
1. Download `subminer-assets.tar.gz`
|
|
||||||
2. Extract and copy `config.example.jsonc` to `~/.config/SubMiner/config.jsonc`
|
|
||||||
3. Copy `plugin/subminer/` directory contents to `~/.config/mpv/scripts/`
|
|
||||||
4. Copy `plugin/subminer.conf` to `~/.config/mpv/script-opts/`
|
|
||||||
5. Copy `assets/themes/subminer.rasi` to:
|
|
||||||
- Linux: `~/.local/share/SubMiner/themes/subminer.rasi`
|
|
||||||
- macOS: `~/Library/Application Support/SubMiner/themes/subminer.rasi`
|
|
||||||
|
|
||||||
Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`.
|
|
||||||
files: |
|
|
||||||
release/*.AppImage
|
release/*.AppImage
|
||||||
release/*.dmg
|
release/*.dmg
|
||||||
|
release/*.exe
|
||||||
release/*.zip
|
release/*.zip
|
||||||
release/*.tar.gz
|
release/*.tar.gz
|
||||||
|
release/*.tar.gz.sha256
|
||||||
|
release/latest*.yml
|
||||||
|
release/*.blockmap
|
||||||
release/SHA256SUMS.txt
|
release/SHA256SUMS.txt
|
||||||
dist/launcher/subminer
|
dist/launcher/subminer
|
||||||
draft: false
|
dist/launcher/subminer.cmd
|
||||||
prerelease: false
|
)
|
||||||
|
|
||||||
|
if [ "${#artifacts[@]}" -eq 0 ]; then
|
||||||
|
echo "No release artifacts found for upload."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for asset in "${artifacts[@]}"; do
|
||||||
|
gh release upload "$RELEASE_VERSION" "$asset" --clobber
|
||||||
|
done
|
||||||
|
|
||||||
|
aur-publish:
|
||||||
|
needs: [release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check AUR publish prerequisites
|
||||||
|
id: aur_prereqs
|
||||||
|
env:
|
||||||
|
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${AUR_SSH_PRIVATE_KEY}" ]; then
|
||||||
|
echo "::warning::Missing AUR_SSH_PRIVATE_KEY; skipping automated AUR publish."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Configure SSH for AUR
|
||||||
|
id: aur_ssh
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if install -dm700 ~/.ssh \
|
||||||
|
&& printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur \
|
||||||
|
&& chmod 600 ~/.ssh/aur \
|
||||||
|
&& ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts \
|
||||||
|
&& chmod 644 ~/.ssh/known_hosts; then
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "::warning::Unable to configure SSH for AUR; skipping automated AUR publish."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Clone AUR repo
|
||||||
|
id: aur_clone
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
attempts=3
|
||||||
|
for attempt in $(seq 1 "$attempts"); do
|
||||||
|
if git clone ssh://aur@aur.archlinux.org/subminer-bin.git aur-subminer-bin; then
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf aur-subminer-bin
|
||||||
|
|
||||||
|
if [ "$attempt" -lt "$attempts" ]; then
|
||||||
|
sleep $((attempt * 15))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "::warning::Unable to clone subminer-bin from AUR after ${attempts} attempts; skipping automated AUR publish."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Download release assets for AUR
|
||||||
|
id: aur_assets
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version="$RELEASE_VERSION"
|
||||||
|
install -dm755 .tmp/aur-release-assets
|
||||||
|
for asset in "SubMiner-${version#v}.AppImage" subminer subminer-assets.tar.gz; do
|
||||||
|
destination=".tmp/aur-release-assets/$asset"
|
||||||
|
if ! curl --fail --silent --show-error --location \
|
||||||
|
--retry 3 --retry-delay 1 --retry-all-errors \
|
||||||
|
--connect-timeout 30 --max-time 600 \
|
||||||
|
--output "$destination.partial" \
|
||||||
|
"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$version/$asset"; then
|
||||||
|
echo "::warning::Unable to download $asset after retries; skipping automated AUR publish."
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
mv "$destination.partial" "$destination"
|
||||||
|
done
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Update AUR packaging metadata
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version_no_v="$RELEASE_VERSION"
|
||||||
|
version_no_v="${version_no_v#v}"
|
||||||
|
cp packaging/aur/subminer-bin/PKGBUILD aur-subminer-bin/PKGBUILD
|
||||||
|
cp packaging/aur/subminer-bin/.SRCINFO aur-subminer-bin/.SRCINFO
|
||||||
|
bash scripts/update-aur-package.sh \
|
||||||
|
--pkg-dir aur-subminer-bin \
|
||||||
|
--version "$RELEASE_VERSION" \
|
||||||
|
--appimage ".tmp/aur-release-assets/SubMiner-${version_no_v}.AppImage" \
|
||||||
|
--wrapper ".tmp/aur-release-assets/subminer" \
|
||||||
|
--assets ".tmp/aur-release-assets/subminer-assets.tar.gz"
|
||||||
|
|
||||||
|
- name: Commit and push AUR update
|
||||||
|
if: steps.aur_prereqs.outputs.skip != 'true' && steps.aur_ssh.outputs.skip != 'true' && steps.aur_clone.outputs.skip != 'true' && steps.aur_assets.outputs.skip != 'true'
|
||||||
|
working-directory: aur-subminer-bin
|
||||||
|
env:
|
||||||
|
GIT_SSH_COMMAND: ssh -i ~/.ssh/aur -o IdentitiesOnly=yes
|
||||||
|
RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if git diff --quiet -- PKGBUILD .SRCINFO; then
|
||||||
|
echo "AUR packaging already up to date."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add PKGBUILD .SRCINFO
|
||||||
|
git commit -m "Update to $RELEASE_VERSION"
|
||||||
|
|
||||||
|
attempts=3
|
||||||
|
for attempt in $(seq 1 "$attempts"); do
|
||||||
|
if git push origin HEAD:master; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$attempt" -lt "$attempts" ]; then
|
||||||
|
sleep $((attempt * 15))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "::warning::Unable to push the AUR update after ${attempts} attempts; GitHub release is published, but subminer-bin needs manual follow-up."
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
/package-lock.json
|
||||||
|
|
||||||
|
# Superpowers brainstorming
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
# Electron build output
|
# Electron build output
|
||||||
out/
|
out/
|
||||||
dist/
|
dist/
|
||||||
release/
|
release/*
|
||||||
|
!release/
|
||||||
|
!release/release-notes.md
|
||||||
|
!release/prerelease-notes.md
|
||||||
|
build/yomitan/
|
||||||
|
coverage/
|
||||||
|
|
||||||
# Launcher build artifact (produced by make build-launcher)
|
# Launcher build artifact (produced by make build-launcher)
|
||||||
/subminer
|
/subminer
|
||||||
|
/main-entry.js
|
||||||
|
/main-entry.js.map
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
@@ -21,9 +32,7 @@ Thumbs.db
|
|||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
**/CLAUDE.md
|
|
||||||
environment.toml
|
environment.toml
|
||||||
**/CLAUDE.md
|
|
||||||
.env
|
.env
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
||||||
@@ -34,5 +43,27 @@ docs/.vitepress/cache/
|
|||||||
docs/.vitepress/dist/
|
docs/.vitepress/dist/
|
||||||
tests/*
|
tests/*
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
.tmp/
|
||||||
.codex/*
|
.codex/*
|
||||||
.agents/*
|
.agents/*
|
||||||
|
!.agents/skills/
|
||||||
|
.agents/skills/*
|
||||||
|
!.agents/skills/subminer-change-verification/
|
||||||
|
!.agents/skills/subminer-release/
|
||||||
|
!.agents/skills/subminer-scrum-master/
|
||||||
|
.agents/skills/subminer-change-verification/*
|
||||||
|
!.agents/skills/subminer-change-verification/SKILL.md
|
||||||
|
!.agents/skills/subminer-change-verification/scripts/
|
||||||
|
.agents/skills/subminer-change-verification/scripts/*
|
||||||
|
!.agents/skills/subminer-change-verification/scripts/classify_subminer_diff.sh
|
||||||
|
!.agents/skills/subminer-change-verification/scripts/verify_subminer_change.sh
|
||||||
|
.agents/skills/subminer-release/*
|
||||||
|
!.agents/skills/subminer-release/SKILL.md
|
||||||
|
.agents/skills/subminer-scrum-master/*
|
||||||
|
!.agents/skills/subminer-scrum-master/SKILL.md
|
||||||
|
favicon.png
|
||||||
|
.claude/*
|
||||||
|
!stats/public/favicon.png
|
||||||
|
|
||||||
|
# Browser-automation session artifacts (page snapshots, console logs, downloads)
|
||||||
|
.playwright-mcp/
|
||||||
@@ -5,6 +5,6 @@
|
|||||||
[submodule "vendor/yomitan-jlpt-vocab"]
|
[submodule "vendor/yomitan-jlpt-vocab"]
|
||||||
path = vendor/yomitan-jlpt-vocab
|
path = vendor/yomitan-jlpt-vocab
|
||||||
url = https://github.com/stephenmk/yomitan-jlpt-vocab
|
url = https://github.com/stephenmk/yomitan-jlpt-vocab
|
||||||
[submodule "yomitan-jlpt-vocab"]
|
[submodule "vendor/subminer-yomitan"]
|
||||||
path = vendor/yomitan-jlpt-vocab
|
path = vendor/subminer-yomitan
|
||||||
url = https://github.com/stephenmk/yomitan-jlpt-vocab
|
url = https://github.com/ksyasuda/subminer-yomitan
|
||||||
@@ -4,3 +4,4 @@ release
|
|||||||
coverage
|
coverage
|
||||||
vendor
|
vendor
|
||||||
*.log
|
*.log
|
||||||
|
src/core/services/tokenizer/__fixtures__/golden/*.json
|
||||||
@@ -1,29 +1,83 @@
|
|||||||
<!-- BACKLOG.MD MCP GUIDELINES START -->
|
# AGENTS.md
|
||||||
|
|
||||||
<CRITICAL_INSTRUCTION>
|
## Internal Docs
|
||||||
|
|
||||||
## BACKLOG WORKFLOW INSTRUCTIONS
|
Start here, then leave this file.
|
||||||
|
|
||||||
This project uses Backlog.md MCP for all task and project management activities.
|
- Internal system of record: [`docs/README.md`](./docs/README.md)
|
||||||
|
- Architecture map: [`docs/architecture/README.md`](./docs/architecture/README.md)
|
||||||
|
- Workflow map: [`docs/workflow/README.md`](./docs/workflow/README.md)
|
||||||
|
- Verification lanes: [`docs/workflow/verification.md`](./docs/workflow/verification.md)
|
||||||
|
- Knowledge-base rules: [`docs/knowledge-base/README.md`](./docs/knowledge-base/README.md)
|
||||||
|
- Release guide: [`docs/RELEASING.md`](./docs/RELEASING.md)
|
||||||
|
|
||||||
**CRITICAL GUIDANCE**
|
`docs-site/` is user-facing. Do not treat it as the canonical internal source of truth.
|
||||||
|
|
||||||
- If your client supports MCP resources, read `backlog://workflow/overview` to understand when and how to use Backlog for this project.
|
`CLAUDE.md` is a symlink to this file; there is one project instruction file, not two.
|
||||||
- If your client only supports tools or the above request fails, call `backlog.get_workflow_overview()` tool to load the tool-oriented overview (it lists the matching guide tools).
|
|
||||||
|
|
||||||
- **First time working here?** Read the overview resource IMMEDIATELY to learn the workflow
|
## Quick Start
|
||||||
- **Already familiar?** You should have the overview cached ("## Backlog.md Overview (MCP)")
|
|
||||||
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
|
||||||
|
|
||||||
These guides cover:
|
- Init workspace: `git submodule update --init --recursive`
|
||||||
|
- Install deps: `make deps` or `bun install` plus `(cd vendor/texthooker-ui && bun install --frozen-lockfile)`
|
||||||
|
- Fast dev loop: `make dev-watch`
|
||||||
|
- Full local run: `bun run dev`
|
||||||
|
- Verbose Electron debug: `electron . --start --dev --log-level debug`
|
||||||
|
|
||||||
- Decision framework for when to create tasks
|
## Build / Test
|
||||||
- Search-first workflow to avoid duplicates
|
|
||||||
- Links to detailed guides for task creation, execution, and finalization
|
|
||||||
- MCP tools reference
|
|
||||||
|
|
||||||
You MUST read the overview resource to understand the complete workflow. The information is NOT summarized here.
|
- Runtime/package manager: Bun; use the version pinned by `package.json`.
|
||||||
|
- Follow [`docs/workflow/verification.md`](./docs/workflow/verification.md) and start with the cheapest sufficient lane.
|
||||||
|
- Full handoff gate for substantial changes:
|
||||||
|
`bun run typecheck`
|
||||||
|
`bun run test:fast`
|
||||||
|
`bun run test:env`
|
||||||
|
`bun run build`
|
||||||
|
`bun run test:smoke:dist`
|
||||||
|
- If `docs-site/` changed, also run:
|
||||||
|
`bun run docs:test`
|
||||||
|
`bun run docs:build`
|
||||||
|
- Prefer `make pretty` and `bun run format:check:src`
|
||||||
|
|
||||||
</CRITICAL_INSTRUCTION>
|
## Change-Specific Checks
|
||||||
|
|
||||||
<!-- BACKLOG.MD MCP GUIDELINES END -->
|
- Config/schema/defaults: `bun run test:config`; if template/defaults changed, `bun run generate:config-example`
|
||||||
|
- Launcher/plugin: `bun run test:launcher` or `bun run test:env`
|
||||||
|
- Runtime-compat / dist-sensitive: `bun run test:runtime:compat`
|
||||||
|
- Stats dashboard UI (`stats/`): `bun run test:stats`
|
||||||
|
- Build/release scripts (`scripts/**`): `bun run test:scripts`
|
||||||
|
- Internal docs, `AGENTS.md`, or repo skills: `bun run test:docs:kb`
|
||||||
|
- User-facing `docs-site/`: `bun run docs:test`, then `bun run docs:build`
|
||||||
|
- macOS mpv window helper: `bun test scripts/get-mpv-window-macos.test.ts`
|
||||||
|
- Test lanes are directory-discovered via `scripts/test-lanes.ts`; never hand-list test files in `package.json`
|
||||||
|
|
||||||
|
## Docs Upkeep
|
||||||
|
|
||||||
|
- Docs ship with the change, not after. If a change alters behavior, defaults, flags, shortcuts, ports, or APIs, update the matching docs in the same PR. Touching code without reconciling its docs is an incomplete change.
|
||||||
|
- Source of truth for config defaults is the generated `config.example.jsonc`. Never write a default value into prose you didn't read from it, and don't restate the same default across multiple docs; cite/link to one place so there's a single thing to update.
|
||||||
|
- Trigger map (touch left → update right):
|
||||||
|
- `src/config/definitions/**` (schema/defaults/template) → `bun run generate:config-example`, then reconcile `docs-site/configuration.md` + any feature doc that cites that default
|
||||||
|
- shortcuts/keybindings (`shortcuts.*`, `keybindings`, `stats.*Key`, `subtitleSidebar.toggleKey`, controller bindings) → `docs-site/shortcuts.md`
|
||||||
|
- CLI flags/subcommands (`src/cli/args.ts`, `launcher/**`) → `docs-site/usage.md` + relevant integration doc
|
||||||
|
- feature behavior (anki / jellyfin / jimaku / anilist / youtube / immersion / stats / websocket / sidebar / character-dictionary / annotations) → matching `docs-site/<feature>.md`
|
||||||
|
- architecture / IPC / workflow / internal process → internal `docs/` (system of record)
|
||||||
|
- feature set / requirements / install flow → `README.md`
|
||||||
|
- Removing or renaming a config key: grep `docs-site/` and `docs/` for the old key and any value it documented; legacy/hidden keys (`LEGACY_HIDDEN_CONFIG_PATHS`) should not appear in user docs as current settings.
|
||||||
|
- Verify after doc edits: `bun run verify:config-example` (if config touched), `bun run docs:test`, `bun run docs:build`.
|
||||||
|
|
||||||
|
## Sensitive Files
|
||||||
|
|
||||||
|
- Launcher source of truth: `launcher/*.ts`
|
||||||
|
- Generated launcher artifact: `dist/launcher/subminer`; never hand-edit it
|
||||||
|
- Repo-root `./subminer` is stale; do not revive it
|
||||||
|
- `bun run build` rebuilds bundled Yomitan from `vendor/subminer-yomitan`
|
||||||
|
- Do not change signing/packaging identifiers unless the task explicitly requires it
|
||||||
|
|
||||||
|
## Release / PR Notes
|
||||||
|
|
||||||
|
- User-visible PRs need reconciled current-outcome fragment(s) in `changes/*.md`. Format and rules live in [`changes/README.md`](./changes/README.md) (`type` + `area` keys required; inspect existing same-PR fragments, then update/remove stale bullets or add only genuinely separate outcomes; apply the `skip-changelog` label to opt out).
|
||||||
|
- User-visible docs changes get a `type: docs` fragment
|
||||||
|
- CI enforces `bun run changelog:lint` and `bun run changelog:pr-check`
|
||||||
|
- PR review helpers:
|
||||||
|
- `gh pr view --json number,title --jq '"PR #\\(.number): \\(.title)"'`
|
||||||
|
- `gh api repos/:owner/:repo/pulls/<num>/comments --paginate`
|
||||||
|
- For CI debugging, inspect runs with `gh run list/view`; rerun or fix only within the requested scope.
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-plugin uninstall uninstall-linux uninstall-macos print-dirs pretty ensure-bun generate-config generate-example-config docs-dev docs docs-preview docs-watch dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop
|
.PHONY: help submodules deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop docs-test docs-build docs-build-versioned docs-dev
|
||||||
|
|
||||||
APP_NAME := subminer
|
APP_NAME := subminer
|
||||||
THEME_SOURCE := assets/themes/subminer.rasi
|
THEME_SOURCE := assets/themes/subminer.rasi
|
||||||
|
THUMBNAILER_SOURCE := assets/thumbnailers/subminer-ffmpegthumbnailer.thumbnailer
|
||||||
LAUNCHER_OUT := dist/launcher/$(APP_NAME)
|
LAUNCHER_OUT := dist/launcher/$(APP_NAME)
|
||||||
THEME_FILE := subminer.rasi
|
THEME_FILE := subminer.rasi
|
||||||
PLUGIN_CONF := plugin/subminer.conf
|
THUMBNAILER_FILE := subminer-ffmpegthumbnailer.thumbnailer
|
||||||
|
|
||||||
# Default install prefix for the wrapper script.
|
# Default install prefix for the wrapper script.
|
||||||
PREFIX ?= $(HOME)/.local
|
PREFIX ?= $(HOME)/.local
|
||||||
@@ -20,15 +21,11 @@ MACOS_DATA_DIR ?= $(HOME)/Library/Application Support/SubMiner
|
|||||||
MACOS_APP_DIR ?= $(HOME)/Applications
|
MACOS_APP_DIR ?= $(HOME)/Applications
|
||||||
MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app
|
MACOS_APP_DEST ?= $(MACOS_APP_DIR)/SubMiner.app
|
||||||
|
|
||||||
# mpv plugin install directories.
|
|
||||||
MPV_CONFIG_DIR ?= $(HOME)/.config/mpv
|
|
||||||
MPV_SCRIPTS_DIR ?= $(MPV_CONFIG_DIR)/scripts
|
|
||||||
MPV_SCRIPT_OPTS_DIR ?= $(MPV_CONFIG_DIR)/script-opts
|
|
||||||
|
|
||||||
# If building from source, the AppImage will typically land in release/.
|
# If building from source, the AppImage will typically land in release/.
|
||||||
APPIMAGE_SRC := $(firstword $(wildcard release/SubMiner-*.AppImage))
|
APPIMAGE_SRC = $(firstword $(wildcard release/SubMiner-*.AppImage))
|
||||||
MACOS_APP_SRC := $(firstword $(wildcard release/*.app release/*/*.app))
|
MACOS_APP_SRC = $(firstword $(wildcard release/*.app release/*/*.app))
|
||||||
MACOS_ZIP_SRC := $(firstword $(wildcard release/SubMiner-*.zip))
|
MACOS_ZIP_SRC = $(firstword $(wildcard release/SubMiner-*.zip))
|
||||||
|
PRERELEASE_NOTES := release/prerelease-notes.md
|
||||||
|
|
||||||
UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown)
|
UNAME_S := $(shell uname -s 2>/dev/null || echo Unknown)
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
@@ -41,6 +38,17 @@ else
|
|||||||
PLATFORM := unknown
|
PLATFORM := unknown
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
WINDOWS_APPDATA ?= $(if $(APPDATA),$(subst \,/,$(APPDATA)),$(HOME)/AppData/Roaming)
|
||||||
|
|
||||||
|
# mpv plugin install directories.
|
||||||
|
ifeq ($(PLATFORM),windows)
|
||||||
|
MPV_CONFIG_DIR ?= $(WINDOWS_APPDATA)/mpv
|
||||||
|
else
|
||||||
|
MPV_CONFIG_DIR ?= $(HOME)/.config/mpv
|
||||||
|
endif
|
||||||
|
MPV_SCRIPTS_DIR ?= $(MPV_CONFIG_DIR)/scripts
|
||||||
|
MPV_SCRIPT_OPTS_DIR ?= $(MPV_CONFIG_DIR)/script-opts
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@printf '%s\n' \
|
@printf '%s\n' \
|
||||||
"Targets:" \
|
"Targets:" \
|
||||||
@@ -56,20 +64,23 @@ help:
|
|||||||
" dev-watch-macos Start watch loop with forced macOS tracker backend" \
|
" dev-watch-macos Start watch loop with forced macOS tracker backend" \
|
||||||
" dev-toggle Toggle overlay in a running local Electron app" \
|
" dev-toggle Toggle overlay in a running local Electron app" \
|
||||||
" dev-stop Stop a running local Electron app" \
|
" dev-stop Stop a running local Electron app" \
|
||||||
" docs-dev Run VitePress docs dev server" \
|
" docs-test Run docs tests" \
|
||||||
" docs-watch Run VitePress docs dev + Backlog browser together" \
|
" docs-build Build the docs site" \
|
||||||
" docs Build VitePress static docs" \
|
" docs-build-versioned Build production versioned docs site" \
|
||||||
" docs-preview Preview built VitePress docs" \
|
" docs-dev Start the docs dev server" \
|
||||||
" install-linux Install Linux wrapper/theme/app artifacts" \
|
" install-linux Install Linux wrapper/theme/app artifacts" \
|
||||||
" install-macos Install macOS wrapper/theme/app artifacts" \
|
" install-macos Install macOS wrapper/theme/app artifacts" \
|
||||||
" install-plugin Install mpv Lua plugin and plugin config" \
|
" install-windows Print Windows packaging/install guidance" \
|
||||||
" generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \
|
" generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \
|
||||||
"" \
|
"" \
|
||||||
"Other targets:" \
|
"Other targets:" \
|
||||||
" deps Install JS dependencies (root + texthooker-ui)" \
|
" submodules Initialize/update git submodules" \
|
||||||
|
" deps Initialize submodules and install JS dependencies (root + stats + texthooker-ui)" \
|
||||||
" uninstall-linux Remove Linux install artifacts" \
|
" uninstall-linux Remove Linux install artifacts" \
|
||||||
" uninstall-macos Remove macOS install artifacts" \
|
" uninstall-macos Remove macOS install artifacts" \
|
||||||
|
" uninstall-windows Remove Windows mpv plugin artifacts" \
|
||||||
" print-dirs Show resolved install locations" \
|
" print-dirs Show resolved install locations" \
|
||||||
|
" lint Lint stats (format check)" \
|
||||||
"" \
|
"" \
|
||||||
"Variables:" \
|
"Variables:" \
|
||||||
" PREFIX=... Override wrapper install prefix (default: $$HOME/.local)" \
|
" PREFIX=... Override wrapper install prefix (default: $$HOME/.local)" \
|
||||||
@@ -78,7 +89,7 @@ help:
|
|||||||
" LINUX_DATA_DIR=... Override Linux app data dir" \
|
" LINUX_DATA_DIR=... Override Linux app data dir" \
|
||||||
" MACOS_DATA_DIR=... Override macOS app data dir" \
|
" MACOS_DATA_DIR=... Override macOS app data dir" \
|
||||||
" MACOS_APP_DIR=... Override macOS app install dir (default: $$HOME/Applications)" \
|
" MACOS_APP_DIR=... Override macOS app install dir (default: $$HOME/Applications)" \
|
||||||
" MPV_CONFIG_DIR=... Override mpv config dir (default: $$HOME/.config/mpv)"
|
" MPV_CONFIG_DIR=... Override mpv config dir (default: $$HOME/.config/mpv or %APPDATA%/mpv on Windows)"
|
||||||
|
|
||||||
print-dirs:
|
print-dirs:
|
||||||
@printf '%s\n' \
|
@printf '%s\n' \
|
||||||
@@ -89,26 +100,38 @@ print-dirs:
|
|||||||
"MACOS_DATA_DIR=$(MACOS_DATA_DIR)" \
|
"MACOS_DATA_DIR=$(MACOS_DATA_DIR)" \
|
||||||
"MACOS_APP_DIR=$(MACOS_APP_DIR)" \
|
"MACOS_APP_DIR=$(MACOS_APP_DIR)" \
|
||||||
"MACOS_APP_DEST=$(MACOS_APP_DEST)" \
|
"MACOS_APP_DEST=$(MACOS_APP_DEST)" \
|
||||||
|
"WINDOWS_APPDATA=$(WINDOWS_APPDATA)" \
|
||||||
|
"MPV_CONFIG_DIR=$(MPV_CONFIG_DIR)" \
|
||||||
|
"MPV_SCRIPTS_DIR=$(MPV_SCRIPTS_DIR)" \
|
||||||
|
"MPV_SCRIPT_OPTS_DIR=$(MPV_SCRIPT_OPTS_DIR)" \
|
||||||
"APPIMAGE_SRC=$(APPIMAGE_SRC)" \
|
"APPIMAGE_SRC=$(APPIMAGE_SRC)" \
|
||||||
"MACOS_APP_SRC=$(MACOS_APP_SRC)" \
|
"MACOS_APP_SRC=$(MACOS_APP_SRC)" \
|
||||||
"MACOS_ZIP_SRC=$(MACOS_ZIP_SRC)"
|
"MACOS_ZIP_SRC=$(MACOS_ZIP_SRC)"
|
||||||
|
|
||||||
deps:
|
submodules:
|
||||||
@$(MAKE) --no-print-directory ensure-bun
|
@git submodule update --init --recursive
|
||||||
|
|
||||||
|
deps: submodules ensure-bun
|
||||||
@bun install
|
@bun install
|
||||||
|
@cd stats && bun install --frozen-lockfile
|
||||||
@cd vendor/texthooker-ui && bun install --frozen-lockfile
|
@cd vendor/texthooker-ui && bun install --frozen-lockfile
|
||||||
|
|
||||||
ensure-bun:
|
ensure-bun:
|
||||||
@command -v bun >/dev/null 2>&1 || { printf '%s\n' "[ERROR] bun not found"; exit 1; }
|
@command -v bun >/dev/null 2>&1 || { printf '%s\n' "[ERROR] bun not found"; exit 1; }
|
||||||
|
|
||||||
pretty: ensure-bun
|
pretty: ensure-bun
|
||||||
@bun run format
|
@bun run format:src
|
||||||
|
@bun run format:stats
|
||||||
|
|
||||||
|
lint: ensure-bun
|
||||||
|
@bun run lint:stats
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@printf '%s\n' "[INFO] Detected platform: $(PLATFORM)"
|
@printf '%s\n' "[INFO] Detected platform: $(PLATFORM)"
|
||||||
@case "$(PLATFORM)" in \
|
@case "$(PLATFORM)" in \
|
||||||
linux) $(MAKE) --no-print-directory build-linux ;; \
|
linux) $(MAKE) --no-print-directory build-linux ;; \
|
||||||
macos) $(MAKE) --no-print-directory build-macos ;; \
|
macos) $(MAKE) --no-print-directory build-macos ;; \
|
||||||
|
windows) printf '%s\n' "[INFO] Windows builds run via: bun run build:win" ;; \
|
||||||
*) printf '%s\n' "[ERROR] Unsupported OS for this Makefile target: $(PLATFORM)"; exit 1 ;; \
|
*) printf '%s\n' "[ERROR] Unsupported OS for this Makefile target: $(PLATFORM)"; exit 1 ;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -117,6 +140,7 @@ install:
|
|||||||
@case "$(PLATFORM)" in \
|
@case "$(PLATFORM)" in \
|
||||||
linux) $(MAKE) --no-print-directory install-linux ;; \
|
linux) $(MAKE) --no-print-directory install-linux ;; \
|
||||||
macos) $(MAKE) --no-print-directory install-macos ;; \
|
macos) $(MAKE) --no-print-directory install-macos ;; \
|
||||||
|
windows) $(MAKE) --no-print-directory install-windows ;; \
|
||||||
*) printf '%s\n' "[ERROR] Unsupported OS for this Makefile target: $(PLATFORM)"; exit 1 ;; \
|
*) printf '%s\n' "[ERROR] Unsupported OS for this Makefile target: $(PLATFORM)"; exit 1 ;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -136,18 +160,20 @@ build-macos-unsigned: deps
|
|||||||
@bun run build:mac:unsigned
|
@bun run build:mac:unsigned
|
||||||
|
|
||||||
build-launcher:
|
build-launcher:
|
||||||
@printf '%s\n' "[INFO] Bundling launcher script"
|
@printf '%s\n' "[INFO] Building launcher runtime artifacts"
|
||||||
@install -d "$(dir $(LAUNCHER_OUT))"
|
@bun run build:launcher
|
||||||
@bun build ./launcher/main.ts --target=bun --packages=bundle --outfile="$(LAUNCHER_OUT)"
|
|
||||||
@if ! head -1 "$(LAUNCHER_OUT)" | grep -q '^#!/usr/bin/env bun'; then \
|
|
||||||
{ printf '#!/usr/bin/env bun\n'; cat "$(LAUNCHER_OUT)"; } > "$(LAUNCHER_OUT).tmp" && mv "$(LAUNCHER_OUT).tmp" "$(LAUNCHER_OUT)"; \
|
|
||||||
fi
|
|
||||||
@chmod +x "$(LAUNCHER_OUT)"
|
|
||||||
@printf '%s\n' "[INFO] Launcher artifact: $(LAUNCHER_OUT)"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@printf '%s\n' "[INFO] Removing build artifacts"
|
@printf '%s\n' "[INFO] Removing build artifacts"
|
||||||
@rm -rf dist release
|
@if [ -f "$(PRERELEASE_NOTES)" ]; then \
|
||||||
|
PRERELEASE_NOTES_BACKUP="$$(mktemp -t subminer-prerelease-notes.XXXXXX)" && \
|
||||||
|
cp "$(PRERELEASE_NOTES)" "$$PRERELEASE_NOTES_BACKUP" && \
|
||||||
|
rm -rf dist release && \
|
||||||
|
install -d release && \
|
||||||
|
mv "$$PRERELEASE_NOTES_BACKUP" "$(PRERELEASE_NOTES)"; \
|
||||||
|
else \
|
||||||
|
rm -rf dist release; \
|
||||||
|
fi
|
||||||
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||||
|
|
||||||
generate-config: ensure-bun
|
generate-config: ensure-bun
|
||||||
@@ -155,21 +181,8 @@ generate-config: ensure-bun
|
|||||||
@bun run electron . --generate-config
|
@bun run electron . --generate-config
|
||||||
|
|
||||||
generate-example-config: ensure-bun
|
generate-example-config: ensure-bun
|
||||||
@bun run build
|
|
||||||
@bun run generate:config-example
|
@bun run generate:config-example
|
||||||
|
|
||||||
docs-dev: ensure-bun
|
|
||||||
@bun run docs:dev
|
|
||||||
|
|
||||||
docs-watch: ensure-bun
|
|
||||||
@bun run docs:watch
|
|
||||||
|
|
||||||
docs: ensure-bun
|
|
||||||
@bun run docs:build
|
|
||||||
|
|
||||||
docs-preview: ensure-bun
|
|
||||||
@bun run docs:preview
|
|
||||||
|
|
||||||
dev-start: ensure-bun
|
dev-start: ensure-bun
|
||||||
@bun run build
|
@bun run build
|
||||||
@bun run electron . --start
|
@bun run electron . --start
|
||||||
@@ -190,20 +203,36 @@ dev-toggle: ensure-bun
|
|||||||
dev-stop: ensure-bun
|
dev-stop: ensure-bun
|
||||||
@bun run electron . --stop
|
@bun run electron . --stop
|
||||||
|
|
||||||
|
docs-test: ensure-bun
|
||||||
|
@bun run docs:test
|
||||||
|
|
||||||
|
docs-build: ensure-bun
|
||||||
|
@bun run docs:build
|
||||||
|
|
||||||
|
docs-build-versioned: ensure-bun
|
||||||
|
@bun run docs:build:versioned
|
||||||
|
|
||||||
|
docs-dev: ensure-bun
|
||||||
|
@bun run docs:dev
|
||||||
|
|
||||||
|
|
||||||
install-linux: build-launcher
|
install-linux: build-launcher
|
||||||
@printf '%s\n' "[INFO] Installing Linux wrapper/theme artifacts"
|
@printf '%s\n' "[INFO] Installing Linux wrapper/support artifacts"
|
||||||
@install -d "$(BINDIR)"
|
@install -d "$(BINDIR)"
|
||||||
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
||||||
@install -d "$(LINUX_DATA_DIR)/themes"
|
@install -d "$(LINUX_DATA_DIR)/themes"
|
||||||
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
@install -d "$(LINUX_DATA_DIR)/thumbnailers"
|
||||||
|
@install -m 0644 "./$(THUMBNAILER_SOURCE)" "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
|
@install -d "$(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
|
@cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/"
|
||||||
@if [ -n "$(APPIMAGE_SRC)" ]; then \
|
@if [ -n "$(APPIMAGE_SRC)" ]; then \
|
||||||
install -m 0755 "$(APPIMAGE_SRC)" "$(BINDIR)/SubMiner.AppImage"; \
|
install -m 0755 "$(APPIMAGE_SRC)" "$(BINDIR)/SubMiner.AppImage"; \
|
||||||
else \
|
else \
|
||||||
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
printf '%s\n' "[WARN] No release/SubMiner-*.AppImage found; skipping AppImage install"; \
|
||||||
printf '%s\n' " Build one with: make build"; \
|
printf '%s\n' " Build one with: make build"; \
|
||||||
fi
|
fi
|
||||||
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
|
|
||||||
install-macos: build-launcher
|
install-macos: build-launcher
|
||||||
@printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts"
|
@printf '%s\n' "[INFO] Installing macOS wrapper/theme/app artifacts"
|
||||||
@@ -211,6 +240,8 @@ install-macos: build-launcher
|
|||||||
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
|
||||||
@install -d "$(MACOS_DATA_DIR)/themes"
|
@install -d "$(MACOS_DATA_DIR)/themes"
|
||||||
@install -m 0644 "./$(THEME_SOURCE)" "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
|
@install -m 0644 "./$(THEME_SOURCE)" "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
@install -d "$(MACOS_DATA_DIR)/plugin/subminer"
|
||||||
|
@cp -R ./plugin/subminer/. "$(MACOS_DATA_DIR)/plugin/subminer/"
|
||||||
@install -d "$(MACOS_APP_DIR)"
|
@install -d "$(MACOS_APP_DIR)"
|
||||||
@if [ -n "$(MACOS_APP_SRC)" ]; then \
|
@if [ -n "$(MACOS_APP_SRC)" ]; then \
|
||||||
rm -rf "$(MACOS_APP_DEST)"; \
|
rm -rf "$(MACOS_APP_DEST)"; \
|
||||||
@@ -226,26 +257,34 @@ install-macos: build-launcher
|
|||||||
fi
|
fi
|
||||||
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_APP_DEST)"
|
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_APP_DEST)"
|
||||||
|
|
||||||
install-plugin:
|
install-windows:
|
||||||
@printf '%s\n' "[INFO] Installing mpv plugin artifacts"
|
@printf '%s\n' "[INFO] Windows builds run via: bun run build:win"
|
||||||
@install -d "$(MPV_SCRIPTS_DIR)"
|
@printf '%s\n' "[INFO] SubMiner-managed mpv launches inject the bundled runtime plugin; no global mpv plugin install is needed."
|
||||||
@rm -f "$(MPV_SCRIPTS_DIR)/subminer.lua"
|
|
||||||
@install -d "$(MPV_SCRIPTS_DIR)/subminer"
|
|
||||||
@install -d "$(MPV_SCRIPT_OPTS_DIR)"
|
|
||||||
@cp -R ./plugin/subminer/. "$(MPV_SCRIPTS_DIR)/subminer/"
|
|
||||||
@install -m 0644 "./$(PLUGIN_CONF)" "$(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
|
||||||
@printf '%s\n' "Installed to:" " $(MPV_SCRIPTS_DIR)/subminer/main.lua" " $(MPV_SCRIPTS_DIR)/subminer/" " $(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
|
||||||
|
|
||||||
# Uninstall behavior kept unchanged by default.
|
uninstall:
|
||||||
uninstall: uninstall-linux
|
@printf '%s\n' "[INFO] Detected platform: $(PLATFORM)"
|
||||||
|
@case "$(PLATFORM)" in \
|
||||||
|
linux) $(MAKE) --no-print-directory uninstall-linux ;; \
|
||||||
|
macos) $(MAKE) --no-print-directory uninstall-macos ;; \
|
||||||
|
windows) $(MAKE) --no-print-directory uninstall-windows ;; \
|
||||||
|
*) printf '%s\n' "[ERROR] Unsupported OS for this Makefile target: $(PLATFORM)"; exit 1 ;; \
|
||||||
|
esac
|
||||||
|
|
||||||
uninstall-linux:
|
uninstall-linux:
|
||||||
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
|
||||||
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
|
@rm -f "$(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)"
|
||||||
|
@rm -rf "$(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
|
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/thumbnailers/$(THUMBNAILER_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer"
|
||||||
|
|
||||||
uninstall-macos:
|
uninstall-macos:
|
||||||
@rm -f "$(BINDIR)/subminer"
|
@rm -f "$(BINDIR)/subminer"
|
||||||
@rm -f "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
|
@rm -f "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
|
||||||
|
@rm -rf "$(MACOS_DATA_DIR)/plugin/subminer"
|
||||||
@rm -rf "$(MACOS_APP_DEST)"
|
@rm -rf "$(MACOS_APP_DEST)"
|
||||||
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_APP_DEST)"
|
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_DATA_DIR)/plugin/subminer" " $(MACOS_APP_DEST)"
|
||||||
|
|
||||||
|
uninstall-windows:
|
||||||
|
@rm -rf "$(MPV_SCRIPTS_DIR)/subminer"
|
||||||
|
@rm -f "$(MPV_SCRIPTS_DIR)/subminer.lua" "$(MPV_SCRIPTS_DIR)/subminer-loader.lua" "$(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
||||||
|
@printf '%s\n' "Removed:" " $(MPV_SCRIPTS_DIR)/subminer" " $(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
|
||||||
@@ -1,104 +1,275 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/SubMiner.png" width="169" alt="SubMiner logo">
|
|
||||||
<h1>SubMiner</h1>
|
|
||||||
<strong>Look up words, mine to Anki, and enrich cards with context — without leaving mpv.</strong>
|
|
||||||
<br /><br />
|
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
<img src="assets/SubMiner.png" width="160" alt="SubMiner logo">
|
||||||
[]()
|
|
||||||
[](https://docs.subminer.moe)
|
# SubMiner
|
||||||
|
|
||||||
|
Look up words with Yomitan, mine them to Anki, and track your immersion without leaving mpv
|
||||||
|
|
||||||
|
[Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
|
||||||
|
|
||||||
|
[](https://github.com/ksyasuda/SubMiner/releases)
|
||||||
|
[](https://github.com/ksyasuda/SubMiner/releases/latest)
|
||||||
|
[](https://aur.archlinux.org/packages/subminer-bin)
|
||||||
|
[](https://github.com/ksyasuda/SubMiner)
|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[](https://www.typescriptlang.org)
|
||||||
|
|
||||||
|
[](https://github.com/user-attachments/assets/7abab8a9-4e4e-4f06-9f3c-9783e15a3807)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
## Features
|
||||||
|
|
||||||
|
### Dictionary Lookups
|
||||||
|
|
||||||
|
Hover over any word in the subtitles to open the full Yomitan popup with definitions, pitch accent, and frequency data. SubMiner bundles its own Yomitan, separate from any browser install.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
<img src="docs-site/public/screenshots/yomitan-lookup.png" width="800" alt="Yomitan dictionary popup over annotated subtitles in mpv">
|
||||||
[](./assets/minecard.mp4)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br>
|
||||||
|
|
||||||
## What it does
|
### Instant Anki Mining
|
||||||
|
|
||||||
SubMiner is an Electron overlay that sits on top of mpv. It turns your video player into a full sentence-mining workstation:
|
Create an Anki card from the exact playback moment with one key press, click, or controller input. SubMiner fills in the sentence, an audio clip, and a screenshot or animated image.
|
||||||
|
|
||||||
- **Hover to look up** — Yomitan dictionary popups directly on subtitles
|
<div align="center">
|
||||||
- **One-key mining** — Creates Anki cards with sentence, audio, screenshot, and translation
|
<img src="docs-site/public/screenshots/one-key-mining.png" width="800" alt="Anki card created from SubMiner with sentence, audio, and screenshot">
|
||||||
- **Instant auto-enrichment** — Optional local AnkiConnect proxy enriches new Yomitan cards immediately
|
</div>
|
||||||
- **Reading annotations** — Combines N+1 targeting, frequency-dictionary highlighting, and JLPT underlining while you read
|
|
||||||
- **Hover-aware playback** — By default, hovering subtitle text pauses mpv and resumes on mouse leave (`subtitleStyle.autoPauseVideoOnHover`)
|
|
||||||
- **Subtitle tools** — Download from Jimaku, sync with alass/ffsubsync
|
|
||||||
- **Immersion tracking** — SQLite-powered stats on your watch time and mining activity
|
|
||||||
- **Custom texthooker page** — Built-in custom texthooker page and websocket, no extra setup
|
|
||||||
- **Jellyfin integration** — Remote playback setup, cast device mode, and direct playback launch
|
|
||||||
- **AniList progress** — Track episode completion and push watching progress automatically
|
|
||||||
|
|
||||||
## Quick start
|
<br>
|
||||||
|
|
||||||
### 1. Install
|
### Reading Annotations
|
||||||
|
|
||||||
**Linux (AppImage):**
|
Subtitles are annotated as they play with frequency highlighting, JLPT tags, N+1 targeting, and character names from a generated dictionary. Particles and grammar-only tokens stay plain so the words worth learning stand out.
|
||||||
|
|
||||||
```bash
|
<div align="center">
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage
|
<img src="docs-site/public/screenshots/annotations.png" width="800" alt="Annotated subtitles with frequency coloring, JLPT underlines, and N+1 targets">
|
||||||
chmod +x ~/.local/bin/SubMiner.AppImage
|
</div>
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~/.local/bin/subminer
|
|
||||||
chmod +x ~/.local/bin/subminer
|
|
||||||
|
|
||||||
```
|
<br>
|
||||||
|
|
||||||
> [!NOTE]
|
### Immersion Dashboard
|
||||||
> The `subminer` wrapper uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
|
|
||||||
|
|
||||||
**From source** or **macOS** — see the [installation guide](https://docs.subminer.moe/installation#from-source).
|
A stats dashboard tracks watch time, vocabulary growth, mining throughput, session history, and trends. Everything stays on your machine, with no third-party tracking.
|
||||||
|
|
||||||
### 2. Install the mpv plugin and configuration file
|
<div align="center">
|
||||||
|
<img src="docs-site/public/screenshots/stats-overview.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
|
||||||
|
</div>
|
||||||
|
|
||||||
```bash
|
<br>
|
||||||
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer-assets.tar.gz -O /tmp/subminer-assets.tar.gz
|
|
||||||
tar -xzf /tmp/subminer-assets.tar.gz -C /tmp
|
|
||||||
mkdir -p ~/.config/mpv/scripts/subminer
|
|
||||||
mkdir -p ~/.config/mpv/script-opts
|
|
||||||
cp -R /tmp/plugin/subminer/. ~/.config/mpv/scripts/subminer/
|
|
||||||
cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/
|
|
||||||
mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Set up Yomitan Dictionaries
|
### Integrations
|
||||||
|
|
||||||
```bash
|
<table>
|
||||||
subminer app --yomitan
|
<tr>
|
||||||
```
|
<td><b>YouTube</b></td>
|
||||||
|
<td>Play YouTube URLs with subtitle tracks picked by your language priorities, or choose tracks yourself in the overlay picker (<code>Ctrl+Alt+C</code>). Requires <code>yt-dlp</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>AniList</b></td>
|
||||||
|
<td>Automatic episode tracking and progress sync</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Jellyfin</b></td>
|
||||||
|
<td>Browse your Jellyfin library, or cast to SubMiner from any Jellyfin client. Setup and discovery live in the tray menu</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Jimaku</b></td>
|
||||||
|
<td>Search and download Japanese subtitles. Requires a free Jimaku API key</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>TsukiHime</b></td>
|
||||||
|
<td>Search and download subtitles extracted from anime releases, with Japanese and secondary-language tabs (<code>Ctrl+Shift+T</code>). Requires <code>xz</code> on your <code>PATH</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>Subtitle generation</b></td>
|
||||||
|
<td>Transcribe a video's audio into Japanese subtitles locally from the generation modal (<code>Ctrl+Shift+G</code>), the subtitle sidebar, or the launcher. SubMiner can download models for you; optional Silero speech detection helps focus on dialogue. Requires whisper.cpp and FFmpeg. <a href="https://docs.subminer.moe/subtitle-generation">Setup guide</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>AniSkip</b></td>
|
||||||
|
<td>Automatic intro detection with chapter markers and a one-key skip (<code>TAB</code> by default)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>alass / ffsubsync</b></td>
|
||||||
|
<td>Retime a subtitle against the audio or another subtitle track (<code>Ctrl+Alt+S</code>). Requires <code>alass</code> or <code>ffsubsync</code>; set <code>subsync.alass_path</code> or <code>subsync.ffsubsync_path</code> if they are not in <code>/usr/bin</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>WebSocket</b></td>
|
||||||
|
<td>Plain subtitle feed plus a dedicated annotated feed for texthooker pages and custom tools</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
### 4. Mine
|
<br>
|
||||||
|
|
||||||
```bash
|
---
|
||||||
subminer app --start --background
|
|
||||||
subminer video.mkv # default plugin config auto-starts visible overlay + resumes playback when ready
|
|
||||||
subminer --start video.mkv # optional explicit overlay start when plugin auto_start=no
|
|
||||||
```
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
| Required | Optional |
|
SubMiner runs on Linux, macOS 11+, and Windows 10+. Only **mpv** is required to run it (plus `fuse2` for the Linux AppImage). Mining cards also needs Anki with the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on. Everything else is optional.
|
||||||
| ------------------------------------------ | -------------------------------------------------- |
|
|
||||||
| `bun` | |
|
| Dependency | Status | What it does |
|
||||||
| `mpv` with IPC socket | `yt-dlp` |
|
| ------------------------ | ---------------- | -------------------------------------------------------------------- |
|
||||||
| `ffmpeg` | `guessit` (better AniSkip title/episode detection) |
|
| mpv | Required | The video player SubMiner draws over |
|
||||||
| `mecab` + `mecab-ipadic` | `fzf` / `rofi` |
|
| fuse2 | Required (Linux) | Running the AppImage |
|
||||||
| Linux: `hyprctl` or `xdotool` + `xwininfo` | `chafa`, `ffmpegthumbnailer` |
|
| Anki + AnkiConnect | Required to mine | Card creation from the Yomitan popup |
|
||||||
| macOS: Accessibility permission | |
|
| ffmpeg | Recommended | Audio clips and screenshots on cards |
|
||||||
|
| MeCab + mecab-ipadic | Recommended | More accurate N+1, JLPT, and frequency highlighting |
|
||||||
|
| xdotool + xwininfo | Required (X11) | Window tracking on desktops other than Hyprland or Sway |
|
||||||
|
| yt-dlp | Optional | YouTube playback |
|
||||||
|
| xz | Optional | TsukiHime subtitle downloads (most Linux distros already have it) |
|
||||||
|
| alass / ffsubsync | Optional | Subtitle sync |
|
||||||
|
| whisper.cpp | Optional | [Subtitle generation](https://docs.subminer.moe/subtitle-generation) |
|
||||||
|
| guessit | Optional | Better title, season, and episode detection for AniSkip and AniList |
|
||||||
|
| fzf / rofi | Optional | Video picker in the `subminer` launcher (rofi is Linux only) |
|
||||||
|
| chafa, ffmpegthumbnailer | Optional | Thumbnail previews in the launcher pickers |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Platform-specific install commands</b></summary>
|
||||||
|
|
||||||
|
**Arch Linux:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S --needed mpv ffmpeg
|
||||||
|
paru -S --needed mecab-git mecab-ipadic # MeCab is only in the AUR
|
||||||
|
```
|
||||||
|
|
||||||
|
On desktops other than Hyprland or Sway, also install `xdotool` and `xorg-xwininfo`.
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install mpv ffmpeg mecab mecab-ipadic
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install shinchiro.mpv
|
||||||
|
winget install Gyan.FFmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reopen your terminal and check `mpv --version` and `ffmpeg -version`. ffmpeg must be on `PATH`; mpv does not have to be. If `mpv` is not found, either add its folder (usually `%LOCALAPPDATA%\Programs\mpv`) to `PATH` or enter the full path to `mpv.exe` during first-run setup.
|
||||||
|
|
||||||
|
[Scoop](https://scoop.sh) is the alternative if you want one package manager for everything. It is the only one that also carries `xz`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scoop bucket add extras
|
||||||
|
scoop install extras/mpv main/ffmpeg main/yt-dlp main/xz
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [installation guide](https://docs.subminer.moe/installation#_1-install-requirements) for Ubuntu, Debian, and Fedora commands and the full optional package lists.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Install SubMiner
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Arch Linux (AUR)</b></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
paru -S subminer-bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Includes the AppImage and the `subminer` command.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Linux (AppImage)</b></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
wget https://github.com/ksyasuda/SubMiner/releases/latest/download/SubMiner.AppImage -O ~/.local/bin/SubMiner.AppImage \
|
||||||
|
&& chmod +x ~/.local/bin/SubMiner.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
The AppImage is all you need. First-run setup can install the optional `subminer` command, which runs on a copy of Bun bundled with the app, so you do not need Bun installed.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>macOS (DMG)</b></summary>
|
||||||
|
|
||||||
|
Download the latest DMG from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
|
||||||
|
|
||||||
|
Then enable SubMiner under **System Settings > Privacy & Security > Accessibility**, or the overlay cannot follow the mpv window. If macOS blocks the first launch, right-click the app and choose **Open**.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Windows</b></summary>
|
||||||
|
|
||||||
|
Download and run the latest installer (`SubMiner-<version>.exe`) from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). A portable `.zip` is also available.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>From source</b></summary>
|
||||||
|
|
||||||
|
See the [build-from-source guide](https://docs.subminer.moe/installation#from-source).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 2. Launch & Set Up
|
||||||
|
|
||||||
|
Start SubMiner and the setup window opens on first launch. It creates your config file, imports Yomitan dictionaries (you need at least one for lookups), and can install the `subminer` command. On Windows it also creates a **SubMiner mpv** shortcut.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer app --setup # AUR
|
||||||
|
~/.local/bin/SubMiner.AppImage --setup # AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
On **macOS**, open `SubMiner.app` from `/Applications`. On **Windows**, run SubMiner from the Start menu. To reopen setup later, run `subminer app --setup`.
|
||||||
|
|
||||||
|
For card creation, keep Anki open with AnkiConnect installed. SubMiner connects to it at its default address with no extra setup.
|
||||||
|
|
||||||
|
### 3. Mine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer video.mkv # play a video with SubMiner
|
||||||
|
subminer /path/to/dir # pick a file with fzf
|
||||||
|
subminer -R /path/to/dir # pick a file with rofi (Linux only)
|
||||||
|
subminer -H # watch history: replay, next, or previous episode
|
||||||
|
subminer doctor # check your setup
|
||||||
|
```
|
||||||
|
|
||||||
|
On **Windows**, double-click the **SubMiner mpv** shortcut or drag a video file onto it.
|
||||||
|
|
||||||
|
Starting mpv some other way? See [Launching mpv yourself](https://docs.subminer.moe/installation#launching-mpv-yourself) for the IPC socket option the overlay needs.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
For full guides on configuration, Anki, Jellyfin, and more, see [docs.subminer.moe](https://docs.subminer.moe).
|
Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more: **[docs.subminer.moe](https://docs.subminer.moe)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
Built on the shoulders of [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner), [texthooker-ui](https://github.com/Renji-XD/texthooker-ui), [mpvacious](https://github.com/Ajatt-Tools/mpvacious), [Anacreon-Script](https://github.com/friedrich-de/Anacreon-Script), and [autosubsync-mpv](https://github.com/joaquintorres/autosubsync-mpv). Subtitles powered by [Jimaku.cc](https://jimaku.cc). Dictionary lookups via [Yomitan](https://github.com/yomidevs/yomitan).
|
SubMiner builds on the work of these open-source projects:
|
||||||
|
|
||||||
|
| Project | Role |
|
||||||
|
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
|
| [ani-skip](https://github.com/synacktraa/ani-skip) | AniSkip API client for anime intro/outro skip timestamps |
|
||||||
|
| [Anacreon-Script](https://github.com/friedrich-de/Anacreon-Script) | Inspiration for the mining workflow |
|
||||||
|
| [asbplayer](https://github.com/killergerbah/asbplayer) | Inspiration for subtitle sidebar and logic for YouTube subtitle parsing |
|
||||||
|
| [Bee's Character Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) | Character name recognition in subtitles |
|
||||||
|
| [Bun](https://github.com/oven-sh/bun) | Bundled runtime for the `subminer` command-line launcher |
|
||||||
|
| [GameSentenceMiner](https://github.com/bpwhelan/GameSentenceMiner) | Inspiration for Electron overlay with Yomitan integration |
|
||||||
|
| [jellyfin-mpv-shim](https://github.com/jellyfin/jellyfin-mpv-shim) | Jellyfin integration |
|
||||||
|
| [Jimaku.cc](https://jimaku.cc) | Japanese subtitle search and downloads |
|
||||||
|
| [Renji's Texthooker Page](https://github.com/Renji-XD/texthooker-ui) | Base for the WebSocket texthooker integration |
|
||||||
|
| [Yomitan](https://github.com/yomidevs/yomitan) | Dictionary engine powering all lookups and the morphological parser |
|
||||||
|
| [yomitan-jlpt-vocab](https://github.com/stephenmk/yomitan-jlpt-vocab) | JLPT level tags for vocabulary |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[GNU General Public License v3.0](LICENSE)
|
SubMiner is released under the [GNU General Public License v3.0](LICENSE).
|
||||||
|
|
||||||
|
Release packages also bundle an unmodified copy of [Bun](https://github.com/oven-sh/bun), which is MIT licensed and statically links JavaScriptCore (LGPL 2.0) and TinyCC (LGPL 2.1). Its license texts and third-party notices ship inside the app under `resources/bun/licenses`, and each release publishes `bun-v1.3.5-source.tar.gz` with the corresponding source. See [Bundled Bun runtime](https://docs.subminer.moe/installation#bundled-bun-runtime).
|
||||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 433 B After Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 23 MiB |
|
Before Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 21 MiB After Width: | Height: | Size: 3.8 MiB |
@@ -0,0 +1,4 @@
|
|||||||
|
[Thumbnailer Entry]
|
||||||
|
TryExec=ffmpegthumbnailer
|
||||||
|
Exec=ffmpegthumbnailer -i %i -o %o -s %s -f
|
||||||
|
MimeType=video/matroska;video/matroska-3d;video/x-matroska;video/x-matroska-3d;
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
project_name: 'SubMiner'
|
|
||||||
default_status: 'To Do'
|
|
||||||
statuses: ['To Do', 'In Progress', 'Done']
|
|
||||||
labels: []
|
|
||||||
definition_of_done: []
|
|
||||||
date_format: yyyy-mm-dd
|
|
||||||
max_column_width: 20
|
|
||||||
default_editor: 'nvim'
|
|
||||||
auto_open_browser: false
|
|
||||||
default_port: 6420
|
|
||||||
remote_operations: true
|
|
||||||
auto_commit: false
|
|
||||||
bypass_git_hooks: false
|
|
||||||
check_active_branches: true
|
|
||||||
active_branch_days: 30
|
|
||||||
task_prefix: 'task'
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-70
|
|
||||||
title: >-
|
|
||||||
Overlay runtime refactor: remove invisible mode and bind visible overlay to
|
|
||||||
mpv subtitles
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 02:38'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- 'commit:a14c9da'
|
|
||||||
- 'commit:74554a3'
|
|
||||||
- 'commit:75442a4'
|
|
||||||
- 'commit:dde51f8'
|
|
||||||
- 'commit:9e4e588'
|
|
||||||
- src/main/overlay-runtime.ts
|
|
||||||
- src/main/runtime/overlay-mpv-sub-visibility.ts
|
|
||||||
- src/renderer/renderer.ts
|
|
||||||
- docs/plans/2026-02-26-secondary-subtitles-main-overlay.md
|
|
||||||
priority: medium
|
|
||||||
ordinal: 1000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Branch-only commits main..HEAD on refactor-overlay (a14c9da through 9e4e588) rebuilt overlay behavior around visible overlay mode and removed legacy invisible overlay paths.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Removed renderer invisible overlay layout/offset helpers and main hover-highlight runtime code paths.
|
|
||||||
- Added explicit overlay-to-mpv subtitle visibility synchronization so visible overlay state controls primary subtitle visibility consistently.
|
|
||||||
- Hardened overlay runtime/bootstrap lifecycle around modal fallback open state and bridge send path edge cases.
|
|
||||||
- Updated plugin/config/docs defaults to reflect visible-overlay-first behavior and subtitle binding controls.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- Large cross-layer refactor touching runtime wiring, renderer event handling, and plugin behavior.
|
|
||||||
- Regression coverage added/updated for overlay runtime, mpv protocol handling, renderer cleanup, and subtitle rendering paths.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Completed and validated in branch commit set before merge. Refactor reduces dead overlay modes, centralizes subtitle visibility behavior, and documents new defaults/constraints.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-71
|
|
||||||
title: >-
|
|
||||||
Anki integration: add local AnkiConnect proxy transport for push-based
|
|
||||||
auto-enrichment
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 02:38'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- src/anki-integration/anki-connect-proxy.ts
|
|
||||||
- src/anki-integration/anki-connect-proxy.test.ts
|
|
||||||
- src/anki-integration.ts
|
|
||||||
- src/config/resolve/anki-connect.ts
|
|
||||||
- src/core/services/tokenizer/yomitan-parser-runtime.ts
|
|
||||||
- src/core/services/tokenizer/yomitan-parser-runtime.test.ts
|
|
||||||
- docs/anki-integration.md
|
|
||||||
- config.example.jsonc
|
|
||||||
priority: medium
|
|
||||||
ordinal: 2000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Current unmerged working-tree changes implement an optional local AnkiConnect-compatible proxy and transport switching for card enrichment.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Added proxy server that forwards AnkiConnect requests and enqueues addNote/addNotes note IDs for post-create enrichment, with de-duplication and loop-configuration protection.
|
|
||||||
- Added follow-up response-shape compatibility handling so proxy enqueue works for both envelope (`{result,error}`) and bare JSON payloads, including `multi` variants.
|
|
||||||
- Added config schema/defaults/resolution for ankiConnect.proxy (enabled, host, port, upstreamUrl) with validation warnings and fallback behavior.
|
|
||||||
- Runtime now supports transport switching (polling vs proxy) and restarts transport when runtime config patches change transport keys.
|
|
||||||
- Added Yomitan default-profile server sync helper to keep bundled parser profile aligned with configured Anki endpoint.
|
|
||||||
- Updated user docs/config examples for proxy mode setup, troubleshooting, and mining workflow behavior.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- New network surface on local host/port; correctness depends on safe proxy upstream configuration and robust response handling.
|
|
||||||
- Tests added for proxy queue behavior, config resolution, and parser sync routines.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Completed implementation in branch working tree; ready to merge once local changes are committed and test gate passes.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-72
|
|
||||||
title: 'macOS config validation UX: show full warning details in native dialog'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 02:38'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- 'commit:cc2f9ef'
|
|
||||||
- src/main/config-validation.ts
|
|
||||||
- src/main/runtime/startup-config.ts
|
|
||||||
- docs/configuration.md
|
|
||||||
priority: low
|
|
||||||
ordinal: 3000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Commit cc2f9ef improves startup config-warning visibility on macOS by ensuring full details are surfaced in the native UI path and reflected in docs.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Config validation/runtime wiring updated so macOS users can access complete warning details instead of truncated notification-only text.
|
|
||||||
- Added/updated tests around config validation and startup config warning flows.
|
|
||||||
- Updated configuration docs to clarify platform-specific warning presentation behavior.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- Low runtime risk; primarily user-facing diagnostics clarity improvement.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Completed small follow-up fix to reduce config-debug friction on macOS.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-73
|
|
||||||
title: 'MPV plugin: split into modules and optimize startup/command runtime'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 20:50'
|
|
||||||
updated_date: '2026-02-28 22:36'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- plugin/subminer/main.lua
|
|
||||||
- plugin/subminer/bootstrap.lua
|
|
||||||
- plugin/subminer/process.lua
|
|
||||||
- plugin/subminer/aniskip.lua
|
|
||||||
- plugin/subminer/environment.lua
|
|
||||||
- plugin/subminer/lifecycle.lua
|
|
||||||
- plugin/subminer/messages.lua
|
|
||||||
- plugin/subminer/ui.lua
|
|
||||||
- plugin/subminer/hover.lua
|
|
||||||
- plugin/subminer/options.lua
|
|
||||||
- plugin/subminer/state.lua
|
|
||||||
- plugin/subminer.conf
|
|
||||||
- scripts/test-plugin-start-gate.lua
|
|
||||||
- scripts/test-plugin-process-start-retries.lua
|
|
||||||
- launcher/commands/playback-command.ts
|
|
||||||
- launcher/mpv.ts
|
|
||||||
- launcher/mpv.test.ts
|
|
||||||
- launcher/smoke.e2e.test.ts
|
|
||||||
- Makefile
|
|
||||||
- package.json
|
|
||||||
- docs/mpv-plugin.md
|
|
||||||
- docs/installation.md
|
|
||||||
- docs/architecture.md
|
|
||||||
- README.md
|
|
||||||
priority: medium
|
|
||||||
ordinal: 4000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Scope: Replace monolithic `plugin/subminer.lua` with modular plugin runtime; optimize command execution paths; align install/docs/tests; fix launcher smoke instability.
|
|
||||||
|
|
||||||
Delivered behavior:
|
|
||||||
|
|
||||||
- Full plugin cutover to `plugin/subminer/main.lua` + module directory (no runtime compatibility shim with old monolith file).
|
|
||||||
- Process/control command path moved toward async subprocess usage for non-start actions (`stop`, `toggle`, `settings`, restart stop leg), reducing synchronous blocking in mpv script runtime.
|
|
||||||
- AniSkip path guarded: lookup runs only in SubMiner context (launcher metadata, explicit script-message refresh, or detected running app), instead of every opened file.
|
|
||||||
- AniSkip lookup pipeline moved to async subprocess calls (no sync `ps`/`curl` on `file-loaded`) with deferred fetch after auto-start and session-level MAL/title/payload caching.
|
|
||||||
- Startup/runtime loading updated with lazy module initialization via bootstrap proxies.
|
|
||||||
- Plugin install flow updated to copy `plugin/subminer/` directory and remove legacy `~/.config/mpv/scripts/subminer.lua` file.
|
|
||||||
- Added plugin gate script wiring to package scripts (`test:plugin:src`) and launcher test flow.
|
|
||||||
- Smoke tests stabilized across sandbox environments where UNIX socket bind can return `EPERM` while preserving normal-path assertions.
|
|
||||||
- Playback command cleanup race fixed when mpv exits before exit-listener registration.
|
|
||||||
|
|
||||||
Risk/impact context:
|
|
||||||
|
|
||||||
- mpv plugin loading path changed from single-file to module directory; packaging/install paths must stay consistent with release assets.
|
|
||||||
- Async control/AniSkip path changes reduce blocking but can surface timing differences; regression checks added for cold start, file-load gating, and explicit refresh behavior.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
AniSkip gate/async update delivered in plugin runtime:
|
|
||||||
|
|
||||||
- `plugin/subminer/lifecycle.lua`: deferred AniSkip fetch and overlay-start trigger.
|
|
||||||
- `plugin/subminer/aniskip.lua`: async lookup pipeline + context guard + session caches.
|
|
||||||
- `plugin/subminer/environment.lua`: async app-running detection with short cache.
|
|
||||||
- `plugin/subminer/messages.lua`: explicit script-message trigger wiring.
|
|
||||||
|
|
||||||
Regression coverage updated:
|
|
||||||
|
|
||||||
- `scripts/test-plugin-start-gate.lua` now verifies:
|
|
||||||
- no sync `ps`/`curl` on non-context file load
|
|
||||||
- no AniSkip network lookup on non-context file load
|
|
||||||
- script-message refresh forces async AniSkip lookup
|
|
||||||
|
|
||||||
Validation run:
|
|
||||||
|
|
||||||
- `bun run test:plugin:src` pass.
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-74
|
|
||||||
title: 'Startup warmups: configurable warmup vs defer with low-power mode'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-27 21:05'
|
|
||||||
updated_date: '2026-03-01 04:14'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
references:
|
|
||||||
- src/types.ts
|
|
||||||
- src/config/definitions/defaults-core.ts
|
|
||||||
- src/config/definitions/options-core.ts
|
|
||||||
- src/config/definitions/template-sections.ts
|
|
||||||
- src/config/resolve/core-domains.ts
|
|
||||||
- src/main/runtime/startup-warmups.ts
|
|
||||||
- src/main/runtime/startup-warmups-main-deps.ts
|
|
||||||
- src/main/runtime/composers/mpv-runtime-composer.ts
|
|
||||||
- src/core/services/startup.ts
|
|
||||||
- src/main.ts
|
|
||||||
- src/config/config.test.ts
|
|
||||||
- src/main/runtime/startup-warmups.test.ts
|
|
||||||
- src/main/runtime/startup-warmups-main-deps.test.ts
|
|
||||||
- src/core/services/app-ready.test.ts
|
|
||||||
priority: medium
|
|
||||||
ordinal: 7000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Add startup warmup controls to allow per-integration warmup or deferred first-use loading.
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- New config section `startupWarmups` with toggles for `mecab`, `yomitanExtension`, `subtitleDictionaries`, and `jellyfinRemoteSession`.
|
|
||||||
- New `startupWarmups.lowPowerMode` policy: defer everything except Yomitan extension.
|
|
||||||
- Keep default behavior as full warmup.
|
|
||||||
- Ensure deferred integrations lazy-load on first real usage path.
|
|
||||||
- Add test coverage for config parsing/defaults and warmup scheduling behavior.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented:
|
|
||||||
|
|
||||||
- Added `startupWarmups` to config types/defaults/options/template/resolve.
|
|
||||||
- Warmup scheduler now uses per-integration gating functions.
|
|
||||||
- Low-power mode now defers MeCab, subtitle dictionaries, and Jellyfin remote session warmups while still warming Yomitan extension.
|
|
||||||
- Tokenization path guarantees lazy first-use init for deferred dependencies (Yomitan extension, MeCab when missing, subtitle dictionaries).
|
|
||||||
- Added/updated tests across config and runtime warmup modules.
|
|
||||||
|
|
||||||
Validation:
|
|
||||||
|
|
||||||
- `bun run test:config:src`
|
|
||||||
- `bun run test:core:src`
|
|
||||||
- `tsc --noEmit`
|
|
||||||
|
|
||||||
Follow-up updates:
|
|
||||||
|
|
||||||
- Startup now triggers warmups earlier in app-ready flow (right after config validation/log-level setup) instead of waiting for initial args/overlay actions. Goal: tokenization warmup is already done or mostly done by first visible-subs toggle.
|
|
||||||
- Tokenization warmup scheduling consolidated as `subtitle-tokenization` stage; when enabled by toggles, it runs Yomitan extension first, then MeCab/dictionary warmups.
|
|
||||||
- Added per-stage debug logs for warmup progress and skip reasons:
|
|
||||||
- `stage start/ready: yomitan-extension`
|
|
||||||
- `stage start/ready: mecab`
|
|
||||||
- `stage start/ready: subtitle-dictionaries`
|
|
||||||
- `stage start/ready: jellyfin-remote-session`
|
|
||||||
- `stage skipped: jellyfin-remote-session (disabled|auto-connect off)`
|
|
||||||
- Added regression tests for stage-level logging and earlier startup ordering:
|
|
||||||
- `src/main/runtime/startup-warmups.test.ts`
|
|
||||||
- `src/main/runtime/startup-warmups-main-deps.test.ts`
|
|
||||||
- `src/core/services/app-ready.test.ts`
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-75
|
|
||||||
title: 'Tokenizer: configurable POS exclusions for N+1 and frequency annotations'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-03-01 01:23'
|
|
||||||
updated_date: '2026-03-01 04:14'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 6000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
N+1 and frequency highlighting should ignore non-learning tokens (e.g., particles/auxiliary forms) based on MeCab POS1 tags, while remaining user-configurable.
|
|
||||||
|
|
||||||
Problem example: for subtitle phrase containing になれば, the highlighted N+1 target should not be the non-useful inflection/token piece when POS indicates an excluded class.
|
|
||||||
|
|
||||||
Implement configurable exclusion defaults with add/remove overrides so users can tune behavior without code changes.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Default exclusion set omits non-useful POS1 classes from both N+1 candidate selection and frequency highlighting.
|
|
||||||
- [x] #2 Users can add extra POS1 exclusions and remove defaults via config.
|
|
||||||
- [x] #3 Tokenizer/annotation tests cover default behavior and config add/remove overrides.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented configurable annotation POS exclusions with defaults+add/remove for both MeCab POS1 and POS2, wired to N+1 candidate selection and frequency highlighting. Added POS2 default exclusion (非自立), expanded POS1 defaults for function words, added Yomitan->MeCab enrichment to carry pos2/pos3 metadata, updated config docs/examples, and added regression tests including になれば case.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-76
|
|
||||||
title: 'Tokenizer: remove POS exclusion config surface and keep hardcoded defaults'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-03-01 02:45'
|
|
||||||
updated_date: '2026-03-01 04:14'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 5000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Remove user-facing config keys for annotation POS exclusions. Keep N+1/frequency POS exclusion behavior as built-in defaults with no config required.
|
|
||||||
|
|
||||||
Scope: remove config parsing/registry/docs/example for annotationFilters.pos1Exclusions/pos2Exclusions while preserving runtime filtering behavior.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 No user-facing config option exists for annotation POS exclusions.
|
|
||||||
- [x] #2 Runtime N+1/frequency exclusion behavior remains active via built-in defaults.
|
|
||||||
- [x] #3 Config/docs/example/tests updated accordingly.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Removed user-facing subtitleStyle.annotationFilters POS exclusion configuration (schema/resolver/options/docs/example). POS-based N+1/frequency filtering now always uses built-in defaults in runtime. Preserved robust exclusion behavior including merged-token overlap POS handling and N+1-only MeCab enrichment path.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-77
|
|
||||||
title: 'Subtitle hover: auto-pause playback with config toggle'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 22:43'
|
|
||||||
updated_date: '2026-02-28 22:43'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 8000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Add a user-facing subtitle config option to pause mpv playback when the cursor hovers subtitle text and resume playback when the cursor leaves.
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
- New config key: `subtitleStyle.autoPauseVideoOnHover`.
|
|
||||||
- Default should be enabled.
|
|
||||||
- Hover pause/resume must not unpause if playback was already paused before hover.
|
|
||||||
- Docs/examples/tests updated.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 `subtitleStyle.autoPauseVideoOnHover` exists and defaults to `true`.
|
|
||||||
- [x] #2 Overlay pauses playback on subtitle hover and resumes on leave only when hover-triggered pause occurred.
|
|
||||||
- [x] #3 Main/renderer IPC exposes pause-state query for safe hover behavior.
|
|
||||||
- [x] #4 Config docs/examples and user docs/readme mention the new behavior and toggle.
|
|
||||||
- [x] #5 Regression tests cover config parsing/validation and hover behavior edge cases.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented `subtitleStyle.autoPauseVideoOnHover` with default `true`, wired through config defaults/resolution/types, renderer state/style, and mouse hover handlers. Added playback pause-state IPC (`getPlaybackPaused`) to avoid false resume when media was already paused. Added renderer hover behavior tests (including race/cancel case) and config/resolve tests. Updated config examples and docs (`README`, usage, shortcuts, mining workflow, configuration) to document default hover pause/resume behavior and disable path.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-78
|
|
||||||
title: 'Launcher + mpv plugin: auto-start visible overlay pause-until-ready and single-start guard'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-02-28 22:45'
|
|
||||||
updated_date: '2026-02-28 22:45'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 9000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Add startup gating behavior for wrapper + mpv plugin flow so playback starts paused when visible overlay auto-start is enabled, then auto-resumes only after subtitle tokenization is ready.
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
- Plugin option `auto_start_pause_until_ready` (default `yes`).
|
|
||||||
- Launcher reads plugin runtime config and starts mpv paused when `auto_start=yes`, `auto_start_visible_overlay=yes`, and `auto_start_pause_until_ready=yes`.
|
|
||||||
- Main process signals readiness via mpv script message after tokenized subtitle delivery.
|
|
||||||
- Prevent duplicate auto-start attempts from showing `SubMiner already running` OSD.
|
|
||||||
- Keep startup/loading OSD messaging visible and update docs/tests.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Launcher reads `auto_start`, `auto_start_visible_overlay`, and `auto_start_pause_until_ready` from `subminer.conf` and starts mpv with `--pause=yes` when all are enabled.
|
|
||||||
- [x] #2 Plugin pauses on eligible auto-start and resumes only on readiness signal or timeout fallback.
|
|
||||||
- [x] #3 Main process emits `script-message subminer-autoplay-ready` after subtitle tokenization is ready.
|
|
||||||
- [x] #4 Auto-start duplicate triggers are idempotent (no duplicate `--start` behavior and no spurious `Already running` OSD for auto-start path).
|
|
||||||
- [x] #5 Docs and regression tests cover defaults, startup gating behavior, and duplicate-start suppression.
|
|
||||||
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Implemented startup pause gate across launcher/plugin/main runtime:
|
|
||||||
- Added plugin runtime config parsing in launcher (`auto_start`, `auto_start_visible_overlay`, `auto_start_pause_until_ready`) and mpv start-paused behavior for eligible runs.
|
|
||||||
- Added plugin auto-play gate state, timeout fallback, and readiness release via `subminer-autoplay-ready` script message.
|
|
||||||
- Added main-process readiness signaling after tokenization delivery, including unpause fallback command path.
|
|
||||||
- Split auto-start visibility control into separate control commands and added duplicate auto-start idempotency guard to suppress repeated auto-start `Already running` noise.
|
|
||||||
- Updated plugin defaults to enabled (`auto_start=yes`, `auto_start_visible_overlay=yes`, `auto_start_pause_until_ready=yes`) and refreshed docs (`README`, usage, launcher, installation, plugin/config docs).
|
|
||||||
- Added/updated regression coverage (`scripts/test-plugin-start-gate.lua`, launcher smoke/unit tests) validating paused startup, readiness resume, and duplicate-start suppression.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-79
|
|
||||||
title: 'Jimaku modal: auto-close after successful subtitle load'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-03-01 13:52'
|
|
||||||
updated_date: '2026-03-01 14:06'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 10000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
Fix Jimaku modal UX so selecting a subtitle file closes the modal automatically once subtitle download+load succeeds.
|
|
||||||
|
|
||||||
Current behavior:
|
|
||||||
- Subtitle file downloads and loads into mpv.
|
|
||||||
- Jimaku modal remains open until manual close.
|
|
||||||
|
|
||||||
Expected behavior:
|
|
||||||
- On successful `jimakuDownloadFile` result, close modal immediately.
|
|
||||||
- Keep error behavior unchanged (stay open + show error).
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Successful subtitle file selection/download in Jimaku closes modal automatically.
|
|
||||||
- [x] #2 Existing error path keeps modal open and shows error.
|
|
||||||
- [x] #3 Regression test covers success auto-close behavior.
|
|
||||||
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Fixed renderer Jimaku success flow to close modal immediately after successful `jimakuDownloadFile` result. Added regression test (`src/renderer/modals/jimaku.test.ts`) that reproduces keyboard file-selection success path and asserts modal close state + `notifyOverlayModalClosed('jimaku')` emission. Kept failure path unchanged.
|
|
||||||
|
|
||||||
Also wired new test into `test:core:src` and `test:core:dist` package scripts.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-80
|
|
||||||
title: 'Jimaku download: rename subtitle to current video basename'
|
|
||||||
status: Done
|
|
||||||
assignee: []
|
|
||||||
created_date: '2026-03-01 14:17'
|
|
||||||
updated_date: '2026-03-01 14:19'
|
|
||||||
labels: []
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
ordinal: 11000
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
|
|
||||||
When user selects a Jimaku subtitle, save subtitle with filename derived from currently playing media filename instead of Jimaku release filename.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
- Current media: `anime.mkv`
|
|
||||||
- Downloaded subtitle extension: `.srt`
|
|
||||||
- Saved subtitle path: `anime.ja.srt`
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
- Apply in Jimaku download IPC path before writing file.
|
|
||||||
- Preserve collision-avoidance behavior (suffix with jimaku entry id/counter when target exists).
|
|
||||||
- Keep mpv load flow unchanged except using renamed path.
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
|
|
||||||
- [x] #1 Jimaku subtitle destination name uses current media basename plus `.ja` and subtitle extension.
|
|
||||||
- [x] #2 Existing duplicate filename conflict handling still works.
|
|
||||||
- [x] #3 Regression tests cover renamed destination path behavior.
|
|
||||||
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
|
|
||||||
Jimaku download path generation now derives subtitle filename from currently playing media basename and keeps subtitle extension from Jimaku file (`anime.mkv` + `.srt` => `anime.ja.srt`). Added pure helper `buildJimakuSubtitleFilenameFromMediaPath` and routed IPC download flow through it before existing duplicate-path conflict handling. Added regression tests for local path, missing extension fallback, and remote URL media paths.
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"version": "1.3.5",
|
||||||
|
"bunRevision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
|
||||||
|
"releaseTagCommit": "fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab",
|
||||||
|
"artifacts": {
|
||||||
|
"darwin-arm64": {
|
||||||
|
"file": "bun-darwin-aarch64.zip",
|
||||||
|
"sha256": "db17588a4aea8804856825d4bead3f05e1f37276ca606f37e369b4f72f35d3fb"
|
||||||
|
},
|
||||||
|
"darwin-x64": {
|
||||||
|
"file": "bun-darwin-x64-baseline.zip",
|
||||||
|
"sha256": "34b9a56b851058dafa1bc9d61233f2c383aa996889bba30b3180f5ccc2cff1b2"
|
||||||
|
},
|
||||||
|
"linux-arm64": {
|
||||||
|
"file": "bun-linux-aarch64.zip",
|
||||||
|
"sha256": "ed01000f85bd97785228ad2845dc92a1860b8054856826d7317690ac8f8ee74b"
|
||||||
|
},
|
||||||
|
"linux-x64": {
|
||||||
|
"file": "bun-linux-x64-baseline.zip",
|
||||||
|
"sha256": "6bddacd6a65855698b9816f2d74871eda4dd0b7fa921140c6445248f94a742fd"
|
||||||
|
},
|
||||||
|
"win32-x64": {
|
||||||
|
"file": "bun-windows-x64-baseline.zip",
|
||||||
|
"sha256": "bf447dcc3b06aba9b9706a9db46fcd65e06a4d47d31439922313825d06eb47ca"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"licenseInventoryStatus": "source-and-notices-pinned-to-binary-revision",
|
||||||
|
"sourceManifest": "build/bun-source-manifest.json",
|
||||||
|
"correspondingSourceAsset": "bun-v1.3.5-source.tar.gz"
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"version": "1.3.5",
|
||||||
|
"releaseTag": "bun-v1.3.5",
|
||||||
|
"releaseTagCommit": "fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab",
|
||||||
|
"bunRevision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
|
||||||
|
"archiveName": "bun-v1.3.5-source.tar.gz",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "bun",
|
||||||
|
"repository": "oven-sh/bun",
|
||||||
|
"revision": "1e86cebd74a5723e818b5c0555276b646bcf0e4c",
|
||||||
|
"destination": "bun",
|
||||||
|
"sha256": "3a766087902a62a4920e5ac5452dbc7143ff97ea19721f945146629fc9f2d82c",
|
||||||
|
"licensePaths": ["LICENSE.md"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WebKit",
|
||||||
|
"repository": "oven-sh/WebKit",
|
||||||
|
"revision": "6d0f3aac0b817cc01a846b3754b21271adedac12",
|
||||||
|
"destination": "bun/vendor/WebKit",
|
||||||
|
"transport": "git-sparse",
|
||||||
|
"exclude": ["JSTests", "LayoutTests", "ManualTests", "PerformanceTests", "WebDriverTests"],
|
||||||
|
"licensePaths": ["Source/JavaScriptCore/COPYING.LIB"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "boringssl",
|
||||||
|
"repository": "oven-sh/boringssl",
|
||||||
|
"revision": "f1ffd9e83d4f5c28a9c70d73f9a4e6fcf310062f",
|
||||||
|
"destination": "bun/vendor/boringssl",
|
||||||
|
"sha256": "af8fd325793bb261c70114d09a24f36092fbe252be8ec040f0054ec9455f0ea3",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brotli",
|
||||||
|
"repository": "google/brotli",
|
||||||
|
"revision": "ed738e842d2fbdf2d6459e39267a633c4a9b2f5d",
|
||||||
|
"upstreamReference": "v1.1.0",
|
||||||
|
"destination": "bun/vendor/brotli",
|
||||||
|
"sha256": "aaa739962a45b508b2e783b915e6b2b57ed3b12bd4b0feac73acfb144dffa54f",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cares",
|
||||||
|
"repository": "c-ares/c-ares",
|
||||||
|
"revision": "3ac47ee46edd8ea40370222f91613fc16c434853",
|
||||||
|
"destination": "bun/vendor/cares",
|
||||||
|
"sha256": "8c94116cb366ae4a44e487da4d9f7e736287d329efa6f88fdf077cd2d0a2e4b8",
|
||||||
|
"licensePaths": ["LICENSE.md"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hdrhistogram",
|
||||||
|
"repository": "HdrHistogram/HdrHistogram_c",
|
||||||
|
"revision": "be60a9987ee48d0abf0d7b6a175bad8d6c1585d1",
|
||||||
|
"destination": "bun/vendor/hdrhistogram",
|
||||||
|
"sha256": "811c5e5ae5303a75ade50688880af6aad5d2f951ec5785f68186bd18635cdfc9",
|
||||||
|
"licensePaths": ["COPYING.txt", "LICENSE.txt"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "highway",
|
||||||
|
"repository": "google/highway",
|
||||||
|
"revision": "ac0d5d297b13ab1b89f48484fc7911082d76a93f",
|
||||||
|
"destination": "bun/vendor/highway",
|
||||||
|
"sha256": "a7a816f4b62a0414ff0d39c0a8875847468dd6f7c9ad71781a24469a756cdea7",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "libarchive",
|
||||||
|
"repository": "libarchive/libarchive",
|
||||||
|
"revision": "9525f90ca4bd14c7b335e2f8c84a4607b0af6bdf",
|
||||||
|
"destination": "bun/vendor/libarchive",
|
||||||
|
"sha256": "944db9ab58a3cbdb5d947db4f04a3cc15f83b3147fcf7830ada592ba8d4a102c",
|
||||||
|
"licensePaths": ["COPYING"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "libdeflate",
|
||||||
|
"repository": "ebiggers/libdeflate",
|
||||||
|
"revision": "c8c56a20f8f621e6a966b716b31f1dedab6a41e3",
|
||||||
|
"destination": "bun/vendor/libdeflate",
|
||||||
|
"sha256": "1e5cc06bdbf3e1245d8b89c9e3588f507e3c8bc53fe8b8229770a9e8661dea81",
|
||||||
|
"licensePaths": ["COPYING"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "libuv",
|
||||||
|
"repository": "libuv/libuv",
|
||||||
|
"revision": "f3ce527ea940d926c40878ba5de219640c362811",
|
||||||
|
"destination": "bun/vendor/libuv",
|
||||||
|
"sha256": "46040d51e8aa86a7c84e377e224d427c35022ec6e3ed8ed399493b0d8574d0cf",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lolhtml",
|
||||||
|
"repository": "cloudflare/lol-html",
|
||||||
|
"revision": "d64457d9ff0143deef025d5df7e8586092b9afb7",
|
||||||
|
"destination": "bun/vendor/lolhtml",
|
||||||
|
"sha256": "893b77b460f4c4f4634c973f72cf35e38ebe30646d14a92db3f50ee7e586c0ad",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lshpack",
|
||||||
|
"repository": "litespeedtech/ls-hpack",
|
||||||
|
"revision": "8905c024b6d052f083a3d11d0a169b3c2735c8a1",
|
||||||
|
"destination": "bun/vendor/lshpack",
|
||||||
|
"sha256": "07d8bf901bb1b15543f38eabd23938519e1210eebadb52f3d651d6ef130ef973",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mimalloc",
|
||||||
|
"repository": "oven-sh/mimalloc",
|
||||||
|
"revision": "1beadf9651a7bfdec6b5367c380ecc3fe1c40d1a",
|
||||||
|
"destination": "bun/vendor/mimalloc",
|
||||||
|
"sha256": "317ec2a83462ece78c344c4955c6fee103b412a8e6a0d6ddf9ec8963ed9c0881",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "picohttpparser",
|
||||||
|
"repository": "h2o/picohttpparser",
|
||||||
|
"revision": "066d2b1e9ab820703db0837a7255d92d30f0c9f5",
|
||||||
|
"destination": "bun/vendor/picohttpparser",
|
||||||
|
"sha256": "637ff2ab6f5c7f7e05a5b5dc393d5cf2fea8d4754fcaceaaf935ffff5c1323ee",
|
||||||
|
"licensePaths": ["picohttpparser.c", "picohttpparser.h"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tinycc",
|
||||||
|
"repository": "oven-sh/tinycc",
|
||||||
|
"revision": "29985a3b59898861442fa3b43f663fc1af2591d7",
|
||||||
|
"destination": "bun/vendor/tinycc",
|
||||||
|
"sha256": "813cc09aafd6cea9c1ae6b745781e6b27576a0c103f49b63b5c8a732bbe3d290",
|
||||||
|
"licensePaths": ["COPYING"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zlib",
|
||||||
|
"repository": "cloudflare/zlib",
|
||||||
|
"revision": "886098f3f339617b4243b286f5ed364b9989e245",
|
||||||
|
"destination": "bun/vendor/zlib",
|
||||||
|
"sha256": "14bf449df8308696af52f87de88f54c9eb9c91ec5e280587f9d37f7a3f6ed9bb",
|
||||||
|
"licensePaths": ["LICENSE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zstd",
|
||||||
|
"repository": "facebook/zstd",
|
||||||
|
"revision": "f8745da6ff1ad1e7bab384bd1f9d742439278e99",
|
||||||
|
"destination": "bun/vendor/zstd",
|
||||||
|
"sha256": "4b0bd1f0cfb25e61b9103c35f27395530ff5b4c0d2513a00fd745849e85ea52c",
|
||||||
|
"licensePaths": ["COPYING", "LICENSE"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
!include "MUI2.nsh"
|
||||||
|
!include "nsDialogs.nsh"
|
||||||
|
|
||||||
|
Var WindowsMpvShortcutStartMenuPath
|
||||||
|
Var WindowsMpvShortcutDesktopPath
|
||||||
|
|
||||||
|
!macro ResolveWindowsMpvShortcutPaths
|
||||||
|
!ifdef MENU_FILENAME
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuPath "$SMPROGRAMS\${MENU_FILENAME}\SubMiner mpv.lnk"
|
||||||
|
!else
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuPath "$SMPROGRAMS\SubMiner mpv.lnk"
|
||||||
|
!endif
|
||||||
|
StrCpy $WindowsMpvShortcutDesktopPath "$DESKTOP\SubMiner mpv.lnk"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!ifndef BUILD_UNINSTALLER
|
||||||
|
Var WindowsMpvShortcutStartMenuCheckbox
|
||||||
|
Var WindowsMpvShortcutDesktopCheckbox
|
||||||
|
Var WindowsMpvShortcutStartMenuEnabled
|
||||||
|
Var WindowsMpvShortcutDesktopEnabled
|
||||||
|
Var WindowsMpvShortcutDefaultsInitialized
|
||||||
|
|
||||||
|
!macro customInit
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuEnabled "1"
|
||||||
|
StrCpy $WindowsMpvShortcutDesktopEnabled "1"
|
||||||
|
StrCpy $WindowsMpvShortcutDefaultsInitialized "0"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro customPageAfterChangeDir
|
||||||
|
PageEx custom
|
||||||
|
PageCallbacks WindowsMpvShortcutPageCreate WindowsMpvShortcutPageLeave
|
||||||
|
Caption " "
|
||||||
|
PageExEnd
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Function HasExistingInstallation
|
||||||
|
ReadRegStr $0 SHELL_CONTEXT "Software\${APP_GUID}" InstallLocation
|
||||||
|
${if} $0 == ""
|
||||||
|
Push "0"
|
||||||
|
${else}
|
||||||
|
Push "1"
|
||||||
|
${endif}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function InitializeWindowsMpvShortcutDefaults
|
||||||
|
${if} $WindowsMpvShortcutDefaultsInitialized == "1"
|
||||||
|
Return
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
!insertmacro ResolveWindowsMpvShortcutPaths
|
||||||
|
Call HasExistingInstallation
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${if} $0 == "1"
|
||||||
|
${if} ${FileExists} "$WindowsMpvShortcutStartMenuPath"
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuEnabled "1"
|
||||||
|
${else}
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuEnabled "0"
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
${if} ${FileExists} "$WindowsMpvShortcutDesktopPath"
|
||||||
|
StrCpy $WindowsMpvShortcutDesktopEnabled "1"
|
||||||
|
${else}
|
||||||
|
StrCpy $WindowsMpvShortcutDesktopEnabled "0"
|
||||||
|
${endif}
|
||||||
|
${else}
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuEnabled "1"
|
||||||
|
StrCpy $WindowsMpvShortcutDesktopEnabled "1"
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
StrCpy $WindowsMpvShortcutDefaultsInitialized "1"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function WindowsMpvShortcutPageCreate
|
||||||
|
Call InitializeWindowsMpvShortcutDefaults
|
||||||
|
|
||||||
|
!insertmacro MUI_HEADER_TEXT "Windows mpv launcher" "Choose where to create the optional SubMiner mpv shortcuts."
|
||||||
|
|
||||||
|
nsDialogs::Create 1018
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${NSD_CreateLabel} 0u 0u 300u 30u "SubMiner mpv launches SubMiner.exe --launch-mpv so people can open mpv with the SubMiner profile from a separate Windows shortcut."
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${NSD_CreateCheckbox} 0u 44u 280u 12u "Create Start Menu shortcut"
|
||||||
|
Pop $WindowsMpvShortcutStartMenuCheckbox
|
||||||
|
${if} $WindowsMpvShortcutStartMenuEnabled == "1"
|
||||||
|
${NSD_Check} $WindowsMpvShortcutStartMenuCheckbox
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
${NSD_CreateCheckbox} 0u 64u 280u 12u "Create Desktop shortcut"
|
||||||
|
Pop $WindowsMpvShortcutDesktopCheckbox
|
||||||
|
${if} $WindowsMpvShortcutDesktopEnabled == "1"
|
||||||
|
${NSD_Check} $WindowsMpvShortcutDesktopCheckbox
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
${NSD_CreateLabel} 0u 90u 300u 24u "Upgrades preserve the current SubMiner mpv shortcut locations instead of recreating shortcuts you already removed."
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
nsDialogs::Show
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Function WindowsMpvShortcutPageLeave
|
||||||
|
${NSD_GetState} $WindowsMpvShortcutStartMenuCheckbox $0
|
||||||
|
${if} $0 == ${BST_CHECKED}
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuEnabled "1"
|
||||||
|
${else}
|
||||||
|
StrCpy $WindowsMpvShortcutStartMenuEnabled "0"
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
${NSD_GetState} $WindowsMpvShortcutDesktopCheckbox $0
|
||||||
|
${if} $0 == ${BST_CHECKED}
|
||||||
|
StrCpy $WindowsMpvShortcutDesktopEnabled "1"
|
||||||
|
${else}
|
||||||
|
StrCpy $WindowsMpvShortcutDesktopEnabled "0"
|
||||||
|
${endif}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
!macro customInstall
|
||||||
|
Call InitializeWindowsMpvShortcutDefaults
|
||||||
|
!insertmacro ResolveWindowsMpvShortcutPaths
|
||||||
|
|
||||||
|
${if} $WindowsMpvShortcutStartMenuEnabled == "1"
|
||||||
|
!ifdef MENU_FILENAME
|
||||||
|
CreateDirectory "$SMPROGRAMS\${MENU_FILENAME}"
|
||||||
|
!endif
|
||||||
|
CreateShortCut "$WindowsMpvShortcutStartMenuPath" "$appExe" "--launch-mpv" "$appExe" 0 "" "" "Launch mpv with the SubMiner profile"
|
||||||
|
# electron-builder's upstream NSIS templates use the same WinShell call for AppUserModelID wiring.
|
||||||
|
# WinShell.dll comes from electron-builder's cached nsis-resources bundle, so bun run build:win needs no extra repo-local setup.
|
||||||
|
ClearErrors
|
||||||
|
WinShell::SetLnkAUMI "$WindowsMpvShortcutStartMenuPath" "${APP_ID}"
|
||||||
|
${else}
|
||||||
|
Delete "$WindowsMpvShortcutStartMenuPath"
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
${if} $WindowsMpvShortcutDesktopEnabled == "1"
|
||||||
|
CreateShortCut "$WindowsMpvShortcutDesktopPath" "$appExe" "--launch-mpv" "$appExe" 0 "" "" "Launch mpv with the SubMiner profile"
|
||||||
|
# ClearErrors keeps the optional AUMI assignment non-fatal if the packaging environment is missing WinShell.
|
||||||
|
ClearErrors
|
||||||
|
WinShell::SetLnkAUMI "$WindowsMpvShortcutDesktopPath" "${APP_ID}"
|
||||||
|
${else}
|
||||||
|
Delete "$WindowsMpvShortcutDesktopPath"
|
||||||
|
${endif}
|
||||||
|
|
||||||
|
System::Call 'Shell32::SHChangeNotify(i 0x8000000, i 0, i 0, i 0)'
|
||||||
|
!macroend
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro customUnInstall
|
||||||
|
!insertmacro ResolveWindowsMpvShortcutPaths
|
||||||
|
Delete "$WindowsMpvShortcutStartMenuPath"
|
||||||
|
Delete "$WindowsMpvShortcutDesktopPath"
|
||||||
|
!macroend
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||||
|
<zip-file>
|
||||||
|
<pe-file path="SubMiner-*.exe" max-matches="unbounded">
|
||||||
|
<authenticode-sign />
|
||||||
|
</pe-file>
|
||||||
|
<zip-file path="SubMiner-*.zip" max-matches="unbounded">
|
||||||
|
<directory path="*">
|
||||||
|
<pe-file path="*.exe" max-matches="unbounded">
|
||||||
|
<authenticode-sign />
|
||||||
|
</pe-file>
|
||||||
|
<pe-file path="*.dll" max-matches="unbounded">
|
||||||
|
<authenticode-sign />
|
||||||
|
</pe-file>
|
||||||
|
<pe-file path="*.node" max-matches="unbounded">
|
||||||
|
<authenticode-sign />
|
||||||
|
</pe-file>
|
||||||
|
</directory>
|
||||||
|
</zip-file>
|
||||||
|
</zip-file>
|
||||||
|
</artifact-configuration>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Changelog Fragments
|
||||||
|
|
||||||
|
Add one `.md` file per user-visible PR in this directory.
|
||||||
|
|
||||||
|
Use this format:
|
||||||
|
|
||||||
|
```md
|
||||||
|
type: added
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Added keyboard navigation for Yomitan popups.
|
||||||
|
- Added auto-pause toggle when opening the popup.
|
||||||
|
```
|
||||||
|
|
||||||
|
For breaking changes, add `breaking: true`:
|
||||||
|
|
||||||
|
```md
|
||||||
|
type: changed
|
||||||
|
area: config
|
||||||
|
breaking: true
|
||||||
|
|
||||||
|
- Renamed `foo.bar` to `foo.baz`.
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `type` required: `added`, `changed`, `fixed`, `docs`, or `internal`
|
||||||
|
- `area` required: short product area like `overlay`, `launcher`, `release`
|
||||||
|
- `breaking` optional: set to `true` to flag as a breaking change
|
||||||
|
- each non-empty body line becomes a bullet
|
||||||
|
- `README.md` is ignored by the generator
|
||||||
|
- if a PR should not produce release notes, apply the `skip-changelog` label instead of adding a fragment
|
||||||
|
|
||||||
|
PR branch workflow:
|
||||||
|
|
||||||
|
- Before adding a fragment or bullet, inspect the `changes/*.md` files already changed in the PR
|
||||||
|
- If the new work fixes, modifies, renames, or supersedes behavior introduced or referenced by that fragment, edit or remove the stale bullet instead of adding follow-up churn
|
||||||
|
- Add a new bullet only when it describes a truly separate user-visible outcome
|
||||||
|
- Multiple fragment files are allowed when one PR has genuinely separate release-note outcomes, but keep them minimized and current
|
||||||
|
|
||||||
|
How fragments turn into a release:
|
||||||
|
|
||||||
|
- At release time, `bun run changelog:build` (and `bun run changelog:prerelease-notes`) pipes every pending fragment through `claude -p` to merge related items, drop noise, and rewrite into a clean user-facing release body. Write fragments as raw, informative notes — don't worry about polished prose, deduping across PRs, or line-by-line phrasing. The polish step handles all of that.
|
||||||
|
- The polish step treats pending fragments as the final release outcome, not prerelease history. If a feature is added and then renamed or fixed before the stable cut, ship the final feature bullet instead of separate prerelease-only breaking/fix entries.
|
||||||
|
- `CHANGELOG.md`, GitHub release notes, and prerelease notes all use one item per feature or area, inline by default: `- **Name**: one or two short sentences.` Items with several separate changes may use a few short nested bullets instead of a run-on sentence. Items say what changed and how to reach it; implementation details, edge cases, and step-by-step usage belong in the docs, not the release body.
|
||||||
|
- `internal` fragments stay in `CHANGELOG.md` (inside a collapsed `<details>` block) but are dropped from the GitHub release notes entirely.
|
||||||
|
- The polished `CHANGELOG.md` and `release/release-notes.md` are committed and reviewed before tagging — edit the Markdown by hand if Claude misses something.
|
||||||
|
|
||||||
|
Prerelease notes:
|
||||||
|
|
||||||
|
- prerelease tags like `v0.11.3-beta.1` and `v0.11.3-rc.1` reuse the current pending fragments to generate `release/prerelease-notes.md`
|
||||||
|
- from the second prerelease of a base version onward, the notes also open with a `## Changes since <previous tag>` section generated from the fragment diff against the previous beta/RC tag; keep fragment edits meaningful. Editorial-only rewording is filtered out of that section, while genuinely changed behavior and deleted fragments (reverted changes) are reported
|
||||||
|
- existing prerelease notes are a reviewed baseline; later prerelease runs should replace stale beta/RC wording with the current outcome instead of appending fix churn
|
||||||
|
- prerelease note generation does not consume fragments and does not update `CHANGELOG.md` or `docs-site/changelog.md`
|
||||||
|
- the final stable release is the point where `bun run changelog:build` consumes fragments into the stable changelog and release notes
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: linux
|
||||||
|
|
||||||
|
- Fixed the AppImage exiting without opening a window when launched inside a sandbox that mounts the image itself, such as `firejail --appimage` (used by the AppImage catalog test). The detached background app now runs straight from the sandbox's squashfs mount instead of trying to re-mount the AppImage over FUSE.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: app
|
||||||
|
|
||||||
|
- SubMiner no longer crashes with "write EPIPE" when the terminal command that started it is stopped with Ctrl+C.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: internal
|
||||||
|
area: release
|
||||||
|
|
||||||
|
- Release notes, prerelease notes, and `CHANGELOG.md` are now generated as short items per feature or area, nesting only when an item has several separate changes.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: docs
|
||||||
|
area: docs
|
||||||
|
|
||||||
|
- Rewrote the docs site to be shorter and easier to scan: pages lead with setup and use, reference material lives in compact tables, and internal detail was cut from user pages.
|
||||||
|
- The configuration reference now has a short explanation and a key/default table for each config block.
|
||||||
|
- Fixed docs that no longer matched current behavior.
|
||||||
|
- Fixed the Arch Linux install command for MeCab, which is only in the AUR (`mecab-git`), and brought the README requirements and quick start in line with the installation guide.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type: fixed
|
||||||
|
area: dictionary
|
||||||
|
|
||||||
|
- Upgraded the desktop runtime to Electron 43.7.7 and added profile guards that block unsupported runtimes and Electron major or minor downgrades before Yomitan storage is loaded. Patch downgrades within the same major and minor version are allowed.
|
||||||
|
- Development launches now use a separate `SubMiner-dev` profile unless production-profile access is explicitly requested.
|
||||||
|
- Automatic character-dictionary changes now stop when a previously non-empty Yomitan profile suddenly reports zero dictionaries.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Keep the subtitle overlay mapped during fullscreen transitions on Hyprland, avoiding focus changes from repeated hide/show calls that can cancel mpv fullscreen entry. Preserve the hide/show refresh on Sway.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Fixed the overlay (subtitles and sidebar) becoming unclickable after mpv goes fullscreen on Hyprland with `mpv.backend: x11`. The launcher no longer strips `HYPRLAND_INSTANCE_SIGNATURE` from the X11 environment, so the overlay can lift Hyprland's fullscreen input block again.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Jellyfin playback now selects the Japanese subtitle track, and starts subtitle annotations, as soon as that track downloads instead of waiting for every other subtitle track. Tracks now download in parallel, so a slow embedded track Jellyfin has to extract no longer delays the primary subtitles.
|
||||||
|
- Jellyfin episodes with image-based embedded subtitles (PGS, DVD, DVB) now auto-select the Japanese and English tracks. SubMiner no longer requests those tracks as text, and a subtitle track that fails to download no longer cancels selection of the others.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: jellyfin
|
||||||
|
|
||||||
|
- Timing review previews and mined card audio now use the audio track you are playing on Jellyfin direct-play streams, instead of the track Jellyfin marks as default.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Fixed mpv key bindings (input.conf and mpv defaults, e.g. `9`/`0` volume) doing nothing while the overlay had focus when the overlay loaded before SubMiner connected to mpv, common in `mpv.backend: x11` mode. The overlay now reloads mpv's bindings once mpv connects.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: added
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Added scroll wheel keys (`WHEEL_UP`, `WHEEL_DOWN`, `WHEEL_LEFT`, `WHEEL_RIGHT`, with modifiers) to `keybindings`, including capture from the settings key editor.
|
||||||
|
- Mouse button and scroll wheel bindings from mpv (`input.conf` and mpv defaults, e.g. double-click fullscreen, wheel volume, back/forward for playlist) now work while the cursor is over the overlay. On Hyprland, where the overlay always receives input, they previously did nothing. SubMiner bindings and the right-click pause still take priority.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: dependencies
|
||||||
|
|
||||||
|
- Updated Electron to 42.10.0 and undici to 7.29.1 to resolve the security advisories blocking CI.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: changed
|
||||||
|
area: release
|
||||||
|
|
||||||
|
- Removed package-size JSON reports from future releases and their CI size comparisons. Package-content validation remains enabled.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: added
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Session help can run the highlighted command during playback: press `Enter` or double-click a row. Hovering a row with the mouse makes it the `Enter` target. Help opened from the tray with no video loaded stays read-only.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: fixed
|
||||||
|
area: stats
|
||||||
|
|
||||||
|
- Fixed sentence mining (single and multi-line) counting each created card twice in session and lifetime stats.
|
||||||
|
- Fixed word and kanji dates after session deletion. Historical "first seen" dates and counts survive retention pruning; new occurrences record observation time so deletion can update dates exactly. Legacy earlier dates are preserved when the remaining history cannot prove their origin. Stale "last seen" dates are repaired once in the background on next launch.
|
||||||
|
- Fixed re-seen words that the vocabulary filter hides still being counted in the new-words charts, so charts and the vocabulary summary now agree. Existing mismatches are repaired by the same background pass.
|
||||||
|
- Fixed Overview totals, charts, activity calendar, and vocabulary summary staying stale after deleting sessions. Failed refreshes keep the dashboard visible and offer a retry.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: docs
|
||||||
|
area: stats
|
||||||
|
|
||||||
|
- Documented the automatic Overview refresh after deleting sessions or session groups and retrying failed refreshes.
|
||||||
|
- Documented discovery-date preservation after retention pruning and the handling of older write-time timestamps.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- On macOS with Bluetooth headphones such as AirPods, the timing review preview cursor now moves smoothly across the selection instead of stuttering and jumping to the end partway through. This affected Opus audio, including Jellyfin streams.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Media timing review trim handles are now thin lines centered on the clip edge, so it's clear where the clip starts and ends and less of the waveform is covered.
|
||||||
|
- Original subtitle boundaries use orange bars without text tags, leaving the waveform unobstructed.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- The subtitle generation modal now works during Windows YouTube playback, where mpv plays a resolved stream URL instead of the YouTube page; it no longer asks you to open a local file or YouTube video, and generated subtitles load into the playing video.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: added
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- Added a YouTube browser window (`subminer youtube`/`subminer yt`, or tray **Browse YouTube**) that keeps your YouTube login across restarts.
|
||||||
|
- Clicking a video plays it in mpv instead of the page; middle-click, Shift/Ctrl-click, or the right-click menu queues it in mpv's playlist.
|
||||||
|
- Queued YouTube videos get the same Japanese subtitle setup when mpv reaches them.
|
||||||
|
- Closing the YouTube window quits a SubMiner started by `subminer youtube` (after mpv closes, if a video is still playing).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- YouTube Japanese subtitles no longer fail with "HTTP 429" on videos where YouTube also lists a machine-translated Japanese track; SubMiner now picks the real track and falls back to yt-dlp if a direct subtitle download is refused.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
type: added
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- Added `youtube.subtitleSource`: set it to `whisper` to transcribe YouTube videos with Whisper instead of downloading YouTube's captions. The default stays `youtube`.
|
||||||
|
- Whisper mode keeps the video paused while it transcribes, with progress in the subtitle generation modal; close the modal to keep watching, or cancel to continue without subtitles.
|
||||||
|
- The subtitle generation modal (`Ctrl+Shift+G`) now also works on YouTube videos, whatever `youtube.subtitleSource` is set to.
|
||||||
|
- Whisper mode downloads a small audio-only stream and deletes it once generation ends, or when you switch videos, close mpv, or quit.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: youtube
|
||||||
|
|
||||||
|
- Fixed Windows YouTube playback dropping the rest of mpv's queue when it advanced to a video queued from the YouTube browser window. The direct stream now replaces only that playlist entry.
|
||||||
@@ -2,22 +2,51 @@
|
|||||||
* SubMiner Example Configuration File
|
* SubMiner Example Configuration File
|
||||||
*
|
*
|
||||||
* This file is auto-generated from src/config/definitions.ts.
|
* This file is auto-generated from src/config/definitions.ts.
|
||||||
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Subtitle Selection
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// Select primary and secondary mpv subtitle tracks from the overlay.
|
||||||
|
// Hot-reload: enabling or disabling updates the session shortcut immediately.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
"subtitleSelection": {
|
||||||
|
"enabled": false // Use the SubMiner modal to select primary and secondary subtitle tracks. When enabled, its shortcut overrides mpv subtitle selection. Values: true | false
|
||||||
|
}, // Select primary and secondary mpv subtitle tracks from the overlay.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Japanese Subtitle Generation
|
||||||
|
// Generate timed Japanese subtitles from local audio using whisper.cpp.
|
||||||
|
// Configure an existing GGML model path or explicitly download a SubMiner-managed model.
|
||||||
|
// Hot-reload: settings apply to the next generation or model download.
|
||||||
|
// ==========================================
|
||||||
|
"subtitleGeneration": {
|
||||||
|
"whisperPath": "", // Optional path override for whisper.cpp. Leave empty to find whisper-cli on PATH.
|
||||||
|
"modelPath": "", // Path to an existing multilingual whisper.cpp GGML model. Leave empty to use a SubMiner-managed model. A configured path always takes precedence.
|
||||||
|
"managedModel": "small", // Multilingual whisper.cpp model to use when modelPath is empty. Download it explicitly from the generation modal or launcher. Values: tiny | tiny-q5_1 | tiny-q8_0 | base | base-q5_1 | base-q8_0 | small | small-q5_1 | small-q8_0 | medium | medium-q5_0 | medium-q8_0 | large-v1 | large-v2 | large-v2-q5_0 | large-v2-q8_0 | large-v3 | large-v3-q5_0 | large-v3-turbo | large-v3-turbo-q5_0 | large-v3-turbo-q8_0
|
||||||
|
"threads": 4, // Positive integer CPU thread count for whisper.cpp Japanese transcription.
|
||||||
|
"ffmpegPath": "", // Optional FFmpeg path override for audio extraction. Leave empty to find ffmpeg on PATH.
|
||||||
|
"ffprobePath": "", // Optional FFprobe path override for audio tracks and timing. Leave empty to find ffprobe on PATH.
|
||||||
|
"vadModelPath": "", // Path to a whisper.cpp Silero VAD model. Enables dialogue-focused generation while retaining uncertain audible sections, which may include songs. Leave empty to transcribe the full audio.
|
||||||
|
"vadPath": "" // Optional speech detector executable override. With vadModelPath configured, leave empty to find whisper-vad-speech-segments or vad-speech-segments on PATH.
|
||||||
|
}, // Generate timed Japanese subtitles from local audio using whisper.cpp.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Visible Overlay Auto-Start
|
||||||
|
// Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.
|
||||||
|
// SubMiner can still auto-start in the background when this is false.
|
||||||
|
// ==========================================
|
||||||
|
"auto_start_overlay": true, // Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner. Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Texthooker Server
|
// Texthooker Server
|
||||||
// Control whether browser opens automatically for texthooker.
|
// Configure texthooker startup launch and browser opening behavior.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"texthooker": {
|
"texthooker": {
|
||||||
"openBrowser": true, // Open browser setting. Values: true | false
|
"launchAtStartup": false, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||||
}, // Control whether browser opens automatically for texthooker.
|
"openBrowser": false // Open the texthooker page in the default browser when the server starts. Values: true | false
|
||||||
|
}, // Configure texthooker startup launch and browser opening behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// WebSocket Server
|
// WebSocket Server
|
||||||
@@ -25,18 +54,162 @@
|
|||||||
// Auto mode disables built-in server if mpv_websocket is detected.
|
// Auto mode disables built-in server if mpv_websocket is detected.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"websocket": {
|
"websocket": {
|
||||||
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
"enabled": false, // Built-in subtitle websocket server mode. Values: auto | true | false
|
||||||
"port": 6677, // Built-in subtitle websocket server port.
|
"port": 6677 // Built-in subtitle websocket server port.
|
||||||
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Annotation WebSocket
|
||||||
|
// Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
|
||||||
|
// Independent from websocket.auto and defaults to port 6678.
|
||||||
|
// ==========================================
|
||||||
|
"annotationWebsocket": {
|
||||||
|
"enabled": false, // Annotated subtitle websocket server enabled state. Values: true | false
|
||||||
|
"port": 6678 // Annotated subtitle websocket server port.
|
||||||
|
}, // Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Logging
|
// Logging
|
||||||
// Controls logging verbosity.
|
// Controls logging verbosity.
|
||||||
// Set to debug for full runtime diagnostics.
|
// Set to debug for full runtime diagnostics.
|
||||||
|
// Hot-reload: logging.level and logging.files apply live while SubMiner is running.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
"level": "warn", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||||
}, // Controls logging verbosity. Keep this as an object; do not replace with a bare string.
|
"rotation": 7, // Number of days of app, launcher, and mpv logs to retain.
|
||||||
|
"files": {
|
||||||
|
"app": true, // Write SubMiner app runtime logs. Values: true | false
|
||||||
|
"launcher": true, // Write launcher command logs. Values: true | false
|
||||||
|
"mpv": false // Write mpv player logs. Enable temporarily when debugging mpv/plugin startup. Values: true | false
|
||||||
|
} // Files setting.
|
||||||
|
}, // Controls logging verbosity.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Controller Support
|
||||||
|
// Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||||
|
// Use Alt+C to pick a preferred controller and remap actions inline with learn mode.
|
||||||
|
// Trigger input mode can be auto, digital-only, or analog-thresholded depending on the controller.
|
||||||
|
// Override controller.buttonIndices when your pad reports non-standard raw button numbers.
|
||||||
|
// ==========================================
|
||||||
|
"controller": {
|
||||||
|
"enabled": false, // Enable overlay controller support through the Chrome Gamepad API. Values: true | false
|
||||||
|
"preferredGamepadId": "", // Preferred controller id saved from the controller config modal.
|
||||||
|
"preferredGamepadLabel": "", // Preferred controller display label saved for diagnostics.
|
||||||
|
"smoothScroll": true, // Use smooth scrolling for controller-driven popup scroll input. Values: true | false
|
||||||
|
"scrollPixelsPerSecond": 900, // Base popup scroll speed for controller stick input.
|
||||||
|
"horizontalJumpPixels": 160, // Popup page-jump distance for controller jump input.
|
||||||
|
"stickDeadzone": 0.2, // Deadzone applied to controller stick axes.
|
||||||
|
"triggerInputMode": "auto", // How controller triggers are interpreted: auto, pressed-only, or thresholded analog. Values: auto | digital | analog
|
||||||
|
"triggerDeadzone": 0.5, // Minimum analog trigger value required when trigger input uses auto or analog mode.
|
||||||
|
"repeatDelayMs": 320, // Delay before repeating held controller actions.
|
||||||
|
"repeatIntervalMs": 120, // Repeat interval for held controller actions.
|
||||||
|
"buttonIndices": {
|
||||||
|
"select": 6, // Raw button index used for the controller select/minus/back button.
|
||||||
|
"buttonSouth": 0, // Raw button index used for controller south/A button input.
|
||||||
|
"buttonEast": 1, // Raw button index used for controller east/B button input.
|
||||||
|
"buttonWest": 2, // Raw button index used for controller west/X button input.
|
||||||
|
"buttonNorth": 3, // Raw button index used for controller north/Y button input.
|
||||||
|
"leftShoulder": 4, // Raw button index used for controller left shoulder input.
|
||||||
|
"rightShoulder": 5, // Raw button index used for controller right shoulder input.
|
||||||
|
"leftStickPress": 9, // Raw button index used for controller L3 input.
|
||||||
|
"rightStickPress": 10, // Raw button index used for controller R3 input.
|
||||||
|
"leftTrigger": 6, // Raw button index used for controller L2 input.
|
||||||
|
"rightTrigger": 7 // Raw button index used for controller R2 input.
|
||||||
|
}, // Semantic button-name reference mapping used for debug output. Updating it does not rewrite existing raw binding descriptors.
|
||||||
|
"bindings": {
|
||||||
|
"toggleLookup": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 0 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for toggling lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"closeLookup": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 1 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for closing lookup. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"toggleKeyboardOnlyMode": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 3 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for toggling keyboard-only mode. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"mineCard": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 2 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for mining the active card. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"quitMpv": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 6 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for quitting mpv. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"previousAudio": {
|
||||||
|
"kind": "none" // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
}, // Controller binding descriptor for previous Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"nextAudio": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 5 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for next Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"playCurrentAudio": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 4 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for playing the current Yomitan audio. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"toggleMpvPause": {
|
||||||
|
"kind": "button", // Discrete binding input source kind. When kind is "axis", set both axisIndex and direction. Values: none | button | axis
|
||||||
|
"buttonIndex": 9 // Raw button index captured for this discrete controller action.
|
||||||
|
}, // Controller binding descriptor for toggling mpv play/pause. Use Alt+C learn mode or set a raw button/axis descriptor manually. If kind is "axis", direction is required.
|
||||||
|
"leftStickHorizontal": {
|
||||||
|
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||||
|
"axisIndex": 0, // Raw axis index captured for this analog controller action.
|
||||||
|
"dpadFallback": "horizontal" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||||
|
}, // Axis binding descriptor used for left/right token selection. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||||
|
"leftStickVertical": {
|
||||||
|
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||||
|
"axisIndex": 1, // Raw axis index captured for this analog controller action.
|
||||||
|
"dpadFallback": "vertical" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||||
|
}, // Axis binding descriptor used for primary popup scrolling. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||||
|
"rightStickHorizontal": {
|
||||||
|
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||||
|
"axisIndex": 3, // Raw axis index captured for this analog controller action.
|
||||||
|
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||||
|
}, // Axis binding descriptor reserved for alternate right-stick mappings. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||||
|
"rightStickVertical": {
|
||||||
|
"kind": "axis", // Analog binding input source kind. Values: none | axis
|
||||||
|
"axisIndex": 4, // Raw axis index captured for this analog controller action.
|
||||||
|
"dpadFallback": "none" // Optional D-pad fallback used when this analog controller action should also read D-pad input. Values: none | horizontal | vertical
|
||||||
|
} // Axis binding descriptor used for popup page jumps. Use Alt+C learn mode or set a raw axis descriptor manually.
|
||||||
|
}, // Raw controller binding descriptors saved by Alt+C learn mode. For discrete axis bindings, kind "axis" requires axisIndex and direction.
|
||||||
|
"profiles": {} // Per-controller binding and button-index overrides keyed by the controller id reported by the Gamepad API.
|
||||||
|
}, // Gamepad support for the visible overlay while keyboard-only mode is active.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Startup Warmups
|
||||||
|
// Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||||
|
// Disable individual warmups to defer load until first real usage.
|
||||||
|
// lowPowerMode defers all warmups except Yomitan extension.
|
||||||
|
// ==========================================
|
||||||
|
"startupWarmups": {
|
||||||
|
"lowPowerMode": false, // Defer startup warmups except Yomitan extension. Values: true | false
|
||||||
|
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
||||||
|
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
||||||
|
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
||||||
|
"jellyfinRemoteSession": false // Warm up Jellyfin remote session at startup. Values: true | false
|
||||||
|
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Updates
|
||||||
|
// Automatic update check behavior.
|
||||||
|
// Manual checks from the tray or launcher are always allowed.
|
||||||
|
// ==========================================
|
||||||
|
"updates": {
|
||||||
|
"enabled": true, // Run automatic update checks in the background. Values: true | false
|
||||||
|
"checkIntervalHours": 24, // Minimum hours between automatic update checks.
|
||||||
|
"notificationType": "overlay", // How SubMiner announces available updates. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system
|
||||||
|
"channel": "stable" // Release channel used for update checks. Values: stable | prerelease
|
||||||
|
}, // Automatic update check behavior.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Notifications
|
||||||
|
// Overlay notification display behavior.
|
||||||
|
// Hot-reload: position changes apply to the next overlay notification.
|
||||||
|
// ==========================================
|
||||||
|
"notifications": {
|
||||||
|
"overlayPosition": "top-right" // Position for in-overlay notification cards. Values: top-left | top | top-right
|
||||||
|
}, // Overlay notification display behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keyboard Shortcuts
|
// Keyboard Shortcuts
|
||||||
@@ -44,50 +217,206 @@
|
|||||||
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
|
// Hot-reload: shortcut changes apply live and update the session help modal on reopen.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Global accelerator that toggles overlay visibility from anywhere on the system. Use null to disable.
|
||||||
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
"copySubtitle": "CommandOrControl+C", // Accelerator that copies the current subtitle line to the clipboard.
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Accelerator that copies consecutive subtitle lines while the multi-copy window stays open.
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
"updateLastCardFromClipboard": "CommandOrControl+V", // Accelerator that updates the last mined Anki card using the current clipboard contents.
|
||||||
"triggerFieldGrouping": "CommandOrControl+G", // Trigger field grouping setting.
|
"triggerFieldGrouping": "CommandOrControl+G", // Accelerator that triggers Kiku field grouping on duplicate cards.
|
||||||
"triggerSubsync": "Ctrl+Alt+S", // Trigger subsync setting.
|
"triggerSubsync": "Ctrl+Alt+S", // Accelerator that triggers subsync against the active subtitle file.
|
||||||
"mineSentence": "CommandOrControl+S", // Mine sentence setting.
|
"mineSentence": "CommandOrControl+S", // Accelerator that mines the current sentence as a new Anki card.
|
||||||
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Mine sentence multiple setting.
|
"mineSentenceMultiple": "CommandOrControl+Shift+S", // Accelerator that mines consecutive sentences while the multi-mine window stays open.
|
||||||
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
|
"multiCopyTimeoutMs": 3000, // Timeout for multi-copy/mine modes.
|
||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Accelerator that toggles the secondary subtitle bar visibility.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Accelerator that marks the last mined card as an audio card.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openCharacterDictionaryManager": "CommandOrControl+D", // Accelerator that opens the character dictionary manager modal.
|
||||||
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Accelerator that opens the runtime options modal.
|
||||||
|
"openJimaku": "Ctrl+Shift+J", // Accelerator that opens the Jimaku subtitle search modal.
|
||||||
|
"openTsukihime": "Ctrl+Shift+T", // Accelerator that opens the TsukiHime subtitle search modal (configured secondary/Japanese primary tabs).
|
||||||
|
"openSubtitleSelection": "g-s", // Open subtitle selection when enabled. Use g-s to press g then s. Set null to unbind.
|
||||||
|
"openSubtitleGeneration": "Ctrl+Shift+G", // Accelerator that opens the standalone Japanese subtitle generation modal.
|
||||||
|
"openSessionHelp": "CommandOrControl+Slash", // Accelerator that opens the session help / keybinding cheatsheet.
|
||||||
|
"openControllerSelect": "Alt+C", // Accelerator that opens the controller selection and learn-mode modal.
|
||||||
|
"openControllerDebug": "Alt+Shift+C", // Accelerator that opens the controller debug modal with live axis/button readouts.
|
||||||
|
"toggleSubtitleSidebar": "Backslash", // Accelerator that toggles the subtitle sidebar visibility.
|
||||||
|
"toggleNotificationHistory": "CommandOrControl+N", // Accelerator that toggles the overlay notification history panel.
|
||||||
|
"appendClipboardVideoToQueue": "CommandOrControl+A" // Accelerator that appends a video path from the clipboard to the mpv playlist.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keybindings (MPV Commands)
|
// Keybindings (MPV Commands)
|
||||||
// Extra keybindings that are merged with built-in defaults.
|
// Default and custom keybindings that are merged with built-in defaults.
|
||||||
// Set command to null to disable a default keybinding.
|
// Set command to null to disable a default keybinding.
|
||||||
// Hot-reload: keybinding changes apply live and update the session help modal on reopen.
|
// Hot-reload: keybinding changes apply live and update the session help modal on reopen.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"keybindings": [], // Extra keybindings that are merged with built-in defaults.
|
"keybindings": [
|
||||||
|
{
|
||||||
|
"key": "Space", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"cycle",
|
||||||
|
"pause"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KeyF", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"cycle",
|
||||||
|
"fullscreen"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KeyJ", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"cycle",
|
||||||
|
"sid"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Shift+KeyJ", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"cycle",
|
||||||
|
"secondary-sid"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ArrowRight", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"seek",
|
||||||
|
5
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ArrowLeft", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"seek",
|
||||||
|
-5
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ArrowUp", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"seek",
|
||||||
|
60
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ArrowDown", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"seek",
|
||||||
|
-60
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Shift+KeyH", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"sub-seek",
|
||||||
|
-1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Shift+KeyL", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"sub-seek",
|
||||||
|
1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+Shift+ArrowLeft", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"sub-step",
|
||||||
|
-1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+Shift+ArrowRight", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"sub-step",
|
||||||
|
1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KeyZ", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"add",
|
||||||
|
"sub-delay",
|
||||||
|
-0.1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Shift+KeyZ", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"add",
|
||||||
|
"sub-delay",
|
||||||
|
0.1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KeyX", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"add",
|
||||||
|
"sub-delay",
|
||||||
|
0.1
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+Alt+KeyC", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"__youtube-picker-open"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+Alt+KeyP", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"__playlist-browser-open"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+Shift+KeyH", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"__replay-subtitle"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+Shift+KeyL", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"__play-next-subtitle"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "KeyQ", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"quit"
|
||||||
|
] // Command setting.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Ctrl+KeyW", // Key setting.
|
||||||
|
"command": [
|
||||||
|
"quit"
|
||||||
|
] // Command setting.
|
||||||
|
}
|
||||||
|
], // Default and custom keybindings that are merged with built-in defaults.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Secondary Subtitles
|
// Secondary Subtitles
|
||||||
// Dual subtitle track options.
|
// Dual subtitle track options.
|
||||||
// Used by subminer YouTube subtitle generation as secondary language preferences.
|
// Used by managed subtitle loading as secondary language preferences for local and YouTube playback.
|
||||||
// Hot-reload: defaultMode updates live while SubMiner is running.
|
// Hot-reload: defaultMode updates live while SubMiner is running.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
"secondarySubLanguages": [], // Language code priority list used to auto-select a secondary subtitle track when available.
|
||||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
"autoLoadSecondarySub": false, // Automatically load a matching secondary subtitle when the primary subtitle loads. Values: true | false
|
||||||
"defaultMode": "hover", // Default mode setting.
|
"defaultMode": "hover" // Default visibility mode for the secondary subtitle bar. Values: hidden | visible | hover
|
||||||
}, // Dual subtitle track options.
|
}, // Dual subtitle track options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Auto Subtitle Sync
|
// Subtitle Sync
|
||||||
// Subsync engine and executable paths.
|
// Subsync engine and executable paths.
|
||||||
|
// Hot-reload: subsync changes apply to the next subtitle sync run.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subsync": {
|
"subsync": {
|
||||||
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
|
"alass_path": "", // Optional absolute path to the alass binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"alass_path": "", // Alass path setting.
|
"ffsubsync_path": "", // Optional absolute path to the ffsubsync binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
"ffmpeg_path": "", // Optional absolute path to the ffmpeg binary used by subsync. Leave empty to auto-discover from PATH.
|
||||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
||||||
}, // Subsync engine and executable paths.
|
}, // Subsync engine and executable paths.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -95,7 +424,7 @@
|
|||||||
// Initial vertical subtitle position from the bottom.
|
// Initial vertical subtitle position from the bottom.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitlePosition": {
|
"subtitlePosition": {
|
||||||
"yPercent": 10, // Y percent setting.
|
"yPercent": 10 // Vertical position of the subtitle overlay expressed as a percentage from the bottom of the screen.
|
||||||
}, // Initial vertical subtitle position from the bottom.
|
}, // Initial vertical subtitle position from the bottom.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -104,166 +433,322 @@
|
|||||||
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
|
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
|
||||||
|
"css": {
|
||||||
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
|
"color": "#cad3f5", // Color setting.
|
||||||
|
"background-color": "transparent", // Background color setting.
|
||||||
|
"font-size": "35px", // Font size setting.
|
||||||
|
"font-weight": "600", // Font weight setting.
|
||||||
|
"font-style": "normal", // Font style setting.
|
||||||
|
"line-height": "1.35", // Line height setting.
|
||||||
|
"letter-spacing": "-0.01em", // Letter spacing setting.
|
||||||
|
"word-spacing": "0", // Word spacing setting.
|
||||||
|
"font-kerning": "normal", // Font kerning setting.
|
||||||
|
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||||
|
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
|
||||||
|
"backdrop-filter": "blur(6px)", // Backdrop filter setting.
|
||||||
|
"--subtitle-hover-token-color": "#f4dbd6", // Subtitle hover token color setting.
|
||||||
|
"--subtitle-hover-token-background-color": "transparent" // Subtitle hover token background color setting.
|
||||||
|
}, // CSS declaration object applied to primary subtitles after normal subtitle style defaults.
|
||||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||||
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in mpv.
|
"autoPauseVideoOnYomitanPopup": true, // Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes. Values: true | false
|
||||||
"hoverTokenBackgroundColor": "rgba(54, 58, 79, 0.84)", // CSS color used for hovered subtitle token background highlight in mpv.
|
"primaryVisibleOnYomitanPopup": true, // Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode. Values: true | false
|
||||||
"fontFamily": "M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
"nameMatchEnabled": false, // Enable character dictionary sync and subtitle token coloring for character-name matches. Values: true | false
|
||||||
"fontSize": 35, // Font size setting.
|
"nameMatchImagesEnabled": false, // Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary. Values: true | false
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"nameMatchColor": "#f5bde6", // Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.
|
||||||
"fontWeight": "600", // Font weight setting.
|
"nPlusOneColor": "#c6a0f6", // Color used for the single N+1 target token subtitle highlight.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
"knownWordColor": "#a6da95", // Color used for known-word subtitle highlights.
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
"knownWordMaturityColors": {
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
"new": "#ee99a0", // Color for known words whose Anki cards are new (never reviewed), when maturity highlighting is enabled.
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
"learning": "#b7bdf8", // Color for known words whose Anki cards are in (re)learning, when maturity highlighting is enabled.
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
"young": "#91d7e3", // Color for known words whose Anki cards are in review below the mature threshold, when maturity highlighting is enabled.
|
||||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting.
|
"mature": "#a6da95" // Color for known words whose Anki cards are at or above the mature interval threshold, when maturity highlighting is enabled.
|
||||||
"fontStyle": "normal", // Font style setting.
|
}, // Known word maturity colors setting.
|
||||||
"backgroundColor": "rgb(30, 32, 48, 0.88)", // Background color setting.
|
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
|
||||||
"nPlusOneColor": "#c6a0f6", // N plus one color setting.
|
|
||||||
"knownWordColor": "#a6da95", // Known word color setting.
|
|
||||||
"jlptColors": {
|
"jlptColors": {
|
||||||
"N1": "#ed8796", // N1 setting.
|
"N1": "#ed8796", // N1 setting.
|
||||||
"N2": "#f5a97f", // N2 setting.
|
"N2": "#f5a97f", // N2 setting.
|
||||||
"N3": "#f9e2af", // N3 setting.
|
"N3": "#f9e2af", // N3 setting.
|
||||||
"N4": "#a6e3a1", // N4 setting.
|
"N4": "#8bd5ca", // N4 setting.
|
||||||
"N5": "#8aadf4", // N5 setting.
|
"N5": "#8aadf4" // N5 setting.
|
||||||
}, // Jlpt colors setting.
|
}, // Jlpt colors setting.
|
||||||
"frequencyDictionary": {
|
"frequencyDictionary": {
|
||||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||||
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, SubMiner searches installed/default frequency-dictionary locations.
|
"sourcePath": "", // Optional absolute path to a frequency dictionary directory. If empty, built-in discovery search paths are used.
|
||||||
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
|
"topX": 10000, // Only color tokens with frequency rank <= topX (default: 10000).
|
||||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||||
"matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface
|
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
|
||||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||||
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
"bandedColors": [
|
||||||
|
"#ed8796",
|
||||||
|
"#f5a97f",
|
||||||
|
"#f9e2af",
|
||||||
|
"#8bd5ca",
|
||||||
|
"#8aadf4"
|
||||||
|
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||||
}, // Frequency dictionary setting.
|
}, // Frequency dictionary setting.
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
"css": {
|
||||||
"fontSize": 24, // Font size setting.
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"color": "#cad3f5", // Color setting.
|
||||||
"lineHeight": 1.35, // Line height setting.
|
"background-color": "transparent", // Background color setting.
|
||||||
"letterSpacing": "-0.01em", // Letter spacing setting.
|
"font-size": "24px", // Font size setting.
|
||||||
"wordSpacing": 0, // Word spacing setting.
|
"font-weight": "600", // Font weight setting.
|
||||||
"fontKerning": "normal", // Font kerning setting.
|
"font-style": "normal", // Font style setting.
|
||||||
"textRendering": "geometricPrecision", // Text rendering setting.
|
"line-height": "1.35", // Line height setting.
|
||||||
"textShadow": "0 3px 10px rgba(0,0,0,0.69)", // Text shadow setting.
|
"letter-spacing": "-0.01em", // Letter spacing setting.
|
||||||
"backgroundColor": "transparent", // Background color setting.
|
"word-spacing": "0", // Word spacing setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
"font-kerning": "normal", // Font kerning setting.
|
||||||
"fontWeight": "normal", // Font weight setting.
|
"text-rendering": "geometricPrecision", // Text rendering setting.
|
||||||
"fontStyle": "normal", // Font style setting.
|
"text-shadow": "-1px -1px 2px rgba(0,0,0,0.95), 1px -1px 2px rgba(0,0,0,0.95), -1px 1px 2px rgba(0,0,0,0.95), 1px 1px 2px rgba(0,0,0,0.95), 0 0 8px rgba(0,0,0,0.5)", // Text shadow setting.
|
||||||
}, // Secondary setting.
|
"backdrop-filter": "blur(6px)" // Backdrop filter setting.
|
||||||
|
} // CSS declaration object applied to secondary subtitles after normal subtitle style defaults.
|
||||||
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Subtitle Sidebar
|
||||||
|
// Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
// Hot-reload: subtitle sidebar changes apply live without restarting SubMiner.
|
||||||
|
// ==========================================
|
||||||
|
"subtitleSidebar": {
|
||||||
|
"enabled": true, // Enable the subtitle sidebar feature for parsed subtitle sources. Values: true | false
|
||||||
|
"autoOpen": false, // Automatically open the subtitle sidebar once during overlay startup. Values: true | false
|
||||||
|
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv. Values: overlay | embedded
|
||||||
|
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
||||||
|
"pauseVideoOnHover": true, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
|
||||||
|
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
|
||||||
|
"css": {
|
||||||
|
"font-family": "Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP", // Font family setting.
|
||||||
|
"color": "#cad3f5", // Color setting.
|
||||||
|
"background-color": "rgba(73, 77, 100, 0.9)", // Background color setting.
|
||||||
|
"font-size": "16px", // Font size setting.
|
||||||
|
"opacity": "0.95", // Opacity setting.
|
||||||
|
"--subtitle-sidebar-max-width": "420px", // Subtitle sidebar max width setting.
|
||||||
|
"--subtitle-sidebar-timestamp-color": "#a5adcb", // Subtitle sidebar timestamp color setting.
|
||||||
|
"--subtitle-sidebar-active-line-color": "#f5bde6", // Subtitle sidebar active line color setting.
|
||||||
|
"--subtitle-sidebar-active-background-color": "rgba(138, 173, 244, 0.22)", // Subtitle sidebar active background color setting.
|
||||||
|
"--subtitle-sidebar-hover-background-color": "rgba(54, 58, 79, 0.84)" // Subtitle sidebar hover background color setting.
|
||||||
|
} // CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.
|
||||||
|
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Shared AI Provider
|
||||||
|
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
// ==========================================
|
||||||
|
"ai": {
|
||||||
|
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
||||||
|
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
||||||
|
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
||||||
|
"model": "openai/gpt-4o-mini", // Default model identifier requested from the shared AI provider.
|
||||||
|
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
||||||
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // Default system prompt sent with shared AI provider requests.
|
||||||
|
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
||||||
|
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// AnkiConnect Integration
|
// AnkiConnect Integration
|
||||||
// Automatic Anki updates and media generation options.
|
// Automatic Anki updates and media generation options.
|
||||||
// Hot-reload: AI translation settings update live while SubMiner is running.
|
// Hot-reload: ankiConnect.ai.enabled, media.normalizeAudio/mirrorMpvVolume/reviewTiming, knownWords, nPlusOne, fields.word/audio/wordAudio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, isKiku.fieldGrouping, isSenren.fieldGrouping, and lapisKiku.wordCardKind update live while SubMiner is running.
|
||||||
|
// Shared AI provider transport settings are read from top-level ai and typically require restart.
|
||||||
// Most other AnkiConnect settings still require restart.
|
// Most other AnkiConnect settings still require restart.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"ankiConnect": {
|
"ankiConnect": {
|
||||||
"enabled": false, // Enable AnkiConnect integration. Values: true | false
|
"enabled": true, // Enable AnkiConnect integration. Values: true | false
|
||||||
"url": "http://127.0.0.1:8765", // Url setting.
|
"url": "http://127.0.0.1:8765", // Base URL of the AnkiConnect HTTP server.
|
||||||
"pollingRate": 3000, // Polling interval in milliseconds.
|
"pollingRate": 3000, // Polling interval in milliseconds.
|
||||||
"proxy": {
|
"proxy": {
|
||||||
"enabled": false, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
||||||
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
||||||
"port": 8766, // Bind port for local AnkiConnect proxy.
|
"port": 8766, // Bind port for local AnkiConnect proxy.
|
||||||
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
||||||
}, // Proxy setting.
|
}, // Proxy setting.
|
||||||
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
"tags": [
|
||||||
|
"SubMiner"
|
||||||
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
|
"deck": "", // Restrict duplicate detection and card enrichment to this Anki deck. Leave empty to use the Yomitan mining deck when available.
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"word": "Expression", // Card field for the mined word or expression text.
|
||||||
"image": "Picture", // Image setting.
|
"audio": "ExpressionAudio", // Card field that receives generated sentence audio.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"wordAudio": "ExpressionAudio", // Existing word-audio field read to time the frozen first frame of animated images. This mapping is only used for synchronization.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"image": "Picture", // Card field that receives the captured screenshot or animated image.
|
||||||
"translation": "SelectionText", // Translation setting.
|
"sentence": "Sentence", // Card field that receives the source sentence text.
|
||||||
|
"miscInfo": "MiscInfo", // Card field that receives the miscellaneous info pattern (see ankiConnect.metadata.pattern).
|
||||||
|
"translation": "SelectionText" // Card field that receives the current selection or translated text.
|
||||||
}, // Fields setting.
|
}, // Fields setting.
|
||||||
"ai": {
|
"ai": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
||||||
"alwaysUseAiTranslation": false, // Always use ai translation setting. Values: true | false
|
"model": "", // Optional model override for Anki AI translation/enrichment flows.
|
||||||
"apiKey": "", // Api key setting.
|
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
||||||
"model": "openai/gpt-4o-mini", // Model setting.
|
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base url setting.
|
|
||||||
"targetLanguage": "English", // Target language setting.
|
|
||||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
"generateAudio": true, // Generate sentence audio for mined cards. Values: true | false
|
||||||
"generateImage": true, // Generate image setting. Values: true | false
|
"generateImage": true, // Generate screenshot or animated image for mined cards. Values: true | false
|
||||||
"imageType": "static", // Image type setting.
|
"imageType": "static", // Image capture type: "static" for a single still frame, "avif" for an animated AVIF. Values: static | avif
|
||||||
"imageFormat": "jpg", // Image format setting.
|
"imageFormat": "jpg", // Encoding format used when imageType is "static". Values: jpg | png | webp
|
||||||
"imageQuality": 92, // Image quality setting.
|
"imageQuality": 92, // Quality (0-100) used for lossy static image encoders.
|
||||||
"animatedFps": 10, // Animated fps setting.
|
"imageMaxWidth": 0, // Maximum width for static images, in pixels. Set to 0 to preserve the source resolution.
|
||||||
"animatedMaxWidth": 640, // Animated max width setting.
|
"imageMaxHeight": 0, // Maximum height for static images, in pixels. Set to 0 to preserve the source resolution.
|
||||||
"animatedCrf": 35, // Animated crf setting.
|
"animatedFps": 10, // Target frame rate for animated AVIF captures.
|
||||||
"audioPadding": 0.5, // Audio padding setting.
|
"animatedMaxWidth": 640, // Maximum width applied to animated AVIF captures.
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"animatedMaxHeight": 0, // Maximum height for animated AVIF captures, in pixels. Set to 0 to preserve aspect ratio.
|
||||||
"maxMediaDuration": 30, // Max media duration setting.
|
"animatedCrf": 35, // Animated AVIF CRF quality target. Lower values produce larger, higher-quality files.
|
||||||
|
"syncAnimatedImageToWordAudio": true, // For animated AVIF images, prepend a frozen first frame matching the existing word-audio duration so motion starts with sentence audio. Values: true | false
|
||||||
|
"normalizeAudio": true, // Normalize generated sentence audio loudness during media extraction. Changes apply live. Values: true | false
|
||||||
|
"mirrorMpvVolume": true, // Apply mpv's current software volume curve to generated sentence audio. Changes apply live. Values: true | false
|
||||||
|
"reviewTiming": false, // Review and preview subtitle media timing before SubMiner creates or enriches a mined card. Values: true | false
|
||||||
|
"audioPadding": 0, // Seconds of padding appended to both ends of generated sentence audio and animated AVIF clips.
|
||||||
|
"fallbackDuration": 3, // Fallback clip duration in seconds when subtitle timing data is unavailable.
|
||||||
|
"maxMediaDuration": 30 // Maximum allowed media clip duration in seconds. 0 disables the cap.
|
||||||
}, // Media setting.
|
}, // Media setting.
|
||||||
|
"knownWords": {
|
||||||
|
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||||
|
"maturityEnabled": false, // Color known-word highlights by Anki card maturity (new, learning, young, mature) instead of a single color. Requires known-word highlighting. Values: true | false
|
||||||
|
"matureThresholdDays": 21, // Card interval in days at which a known word counts as mature (Anki convention: 21).
|
||||||
|
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
||||||
|
"addMinedWordsImmediately": true, // Immediately append newly mined card words into the known-word cache. Values: true | false
|
||||||
|
"matchMode": "headword", // Known-word matching strategy for subtitle annotations. Cache matches always receive known-word highlighting even when POS filters suppress other annotation types. Values: headword | surface
|
||||||
|
"decks": {} // Decks and expression/word fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word"] }. Reading fields (Reading, Word Reading, ExpressionReading) are always probed so cached words match only in the reading their note teaches; words from notes without readings match in any reading.
|
||||||
|
}, // Known words setting.
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
"overwriteAudio": true, // When updating an existing card, overwrite the audio field instead of skipping it. Values: true | false
|
||||||
"overwriteImage": true, // Overwrite image setting. Values: true | false
|
"overwriteImage": true, // When updating an existing card, overwrite the image field instead of skipping it. Values: true | false
|
||||||
"mediaInsertMode": "append", // Media insert mode setting.
|
"mediaInsertMode": "append", // Whether new media is appended after or prepended before existing field contents on update. Values: append | prepend
|
||||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
"highlightWord": true, // Bold the mined word inside the sentence field on the saved Anki card. Values: true | false
|
||||||
"notificationType": "osd", // Notification type setting.
|
"notificationType": "overlay", // Notification surface used to announce mining and update outcomes. overlay shows notifications on the overlay, system uses OS notifications, both uses overlay and system. osd and osd-system are legacy config-file-only values. Values: overlay | system | both | none | osd | osd-system
|
||||||
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
|
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||||
}, // Behavior setting.
|
}, // Behavior setting.
|
||||||
"nPlusOne": {
|
"nPlusOne": {
|
||||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
"enabled": false, // Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data. Values: true | false
|
||||||
"refreshMinutes": 1440, // Minutes between known-word cache refreshes.
|
"minSentenceWords": 3 // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||||
"matchMode": "headword", // Known-word matching strategy for N+1 highlighting. Values: headword | surface
|
|
||||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
|
||||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
|
||||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
|
||||||
"knownWord": "#a6da95", // Color used for legacy known-word highlights.
|
|
||||||
}, // N plus one setting.
|
}, // N plus one setting.
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"pattern": "[SubMiner] %f (%t)", // Pattern setting.
|
"pattern": "[SubMiner] %f (%t)" // Template used to render the miscInfo field. Placeholders include %f (filename) and %t (timestamp).
|
||||||
}, // Metadata setting.
|
}, // Metadata setting.
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enable Lapis-specific mining behaviors and sentence card model targeting. Values: true | false
|
||||||
"sentenceCardModel": "Japanese sentences", // Sentence card model setting.
|
"sentenceCardModel": "Lapis" // Note type name used by Lapis sentence cards.
|
||||||
}, // Is lapis setting.
|
}, // Is lapis setting.
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enable Kiku-specific mining behaviors (duplicate handling, field grouping). Values: true | false
|
||||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||||
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
|
"deleteDuplicateInAuto": true // When Kiku field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
|
||||||
}, // Is kiku setting.
|
}, // Is kiku setting.
|
||||||
|
"isSenren": {
|
||||||
|
"enabled": false, // Enable Senren-specific duplicate handling (scene-switching field grouping, including miscInfo grouping). Mutually exclusive with isKiku.enabled. Values: true | false
|
||||||
|
"fieldGrouping": "auto", // Senren duplicate-card field grouping mode (scene switching). Values: auto | manual | disabled
|
||||||
|
"deleteDuplicateInAuto": true // When Senren field grouping is "auto", delete the duplicate source card after grouping completes. Values: true | false
|
||||||
|
}, // Is senren setting.
|
||||||
|
"lapisKiku": {
|
||||||
|
"wordCardKind": "word-and-sentence" // Card-type flag SubMiner marks on Kiku/Lapis word cards. Only one flag is set at a time; the others are cleared. Requires isKiku.enabled or isLapis.enabled. Values: word-and-sentence | click | sentence | audio | none
|
||||||
|
} // Lapis kiku setting.
|
||||||
}, // Automatic Anki updates and media generation options.
|
}, // Automatic Anki updates and media generation options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Jimaku
|
// Jimaku
|
||||||
// Jimaku API configuration and defaults.
|
// Jimaku API configuration and defaults.
|
||||||
|
// Hot-reload: Jimaku changes apply to the next Jimaku request.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
"apiBaseUrl": "https://jimaku.cc", // Base URL of the Jimaku subtitle search API.
|
||||||
|
"apiKey": "", // Jimaku API key. Optional but recommended for higher rate limits. Get one for free at https://jimaku.cc.
|
||||||
|
"apiKeyCommand": "", // Shell command that prints the Jimaku API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
|
||||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||||
"maxEntryResults": 10, // Maximum Jimaku search results returned.
|
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||||
}, // Jimaku API configuration and defaults.
|
}, // Jimaku API configuration and defaults.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// YouTube Subtitle Generation
|
// TsukiHime
|
||||||
// Defaults for subminer YouTube subtitle extraction/transcription mode.
|
// TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
|
||||||
|
// Hot-reload: TsukiHime changes apply to the next TsukiHime request.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"youtubeSubgen": {
|
"tsukihime": {
|
||||||
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
|
"apiBaseUrl": "https://api.tsukihime.org/v1", // Base URL of the TsukiHime API (Animetosho successor). No API key required.
|
||||||
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
|
"maxSearchResults": 10 // Maximum TsukiHime search results returned.
|
||||||
"whisperModel": "", // Path to whisper model used for fallback transcription.
|
}, // TsukiHime subtitle search configuration for Japanese primary and configured secondary subtitles. No API key required.
|
||||||
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher.
|
|
||||||
}, // Defaults for subminer YouTube subtitle extraction/transcription mode.
|
// ==========================================
|
||||||
|
// TMDB
|
||||||
|
// TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
|
||||||
|
// Hot-reload: TMDB changes apply to the next TMDB request.
|
||||||
|
// ==========================================
|
||||||
|
"tmdb": {
|
||||||
|
"apiKey": "", // Your own TMDB API key or read access token for live-action posters and synopses in the stats Library. Release builds bundle a project key, so set this only to use your own quota or when running from source (free under Settings > API on themoviedb.org).
|
||||||
|
"apiKeyCommand": "" // Shell command that prints the TMDB API key to stdout. Used instead of apiKey to avoid storing the key in plain text.
|
||||||
|
}, // TMDB (The Movie Database) metadata for live-action dramas and movies in the stats Library: posters, synopses, and grouping by show.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// YouTube Playback Settings
|
||||||
|
// Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||||
|
// Hot-reload: primarySubLanguages and subtitleSource apply to the next YouTube subtitle load.
|
||||||
|
// ==========================================
|
||||||
|
"youtube": {
|
||||||
|
"primarySubLanguages": [
|
||||||
|
"ja",
|
||||||
|
"jpn"
|
||||||
|
], // Comma-separated primary subtitle language priority for managed subtitle auto-selection.
|
||||||
|
"subtitleSource": "youtube", // Where primary YouTube subtitles come from. Whisper transcribes the audio locally using the subtitleGeneration settings. Values: youtube | whisper
|
||||||
|
"mediaCache": {
|
||||||
|
"mode": "direct", // How YouTube card audio/images are extracted. Values: direct | background
|
||||||
|
"maxHeight": 720 // Maximum video height downloaded for the YouTube background media cache. Set to 0 for unlimited.
|
||||||
|
} // Media cache setting.
|
||||||
|
}, // Defaults for managed subtitle language preferences and YouTube subtitle loading.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Anilist
|
// Anilist
|
||||||
// Anilist API credentials and update behavior.
|
// Anilist API credentials and update behavior.
|
||||||
|
// Includes optional auto-sync for a merged MRU-based character dictionary in bundled Yomitan.
|
||||||
|
// Character dictionaries are keyed by AniList media ID (no season/franchise merge).
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"anilist": {
|
"anilist": {
|
||||||
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false
|
||||||
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
|
||||||
|
"characterDictionary": {
|
||||||
|
"maxLoaded": 3, // Maximum number of most-recently-used anime snapshots included in the merged Yomitan character dictionary.
|
||||||
|
"profileScope": "all", // Yomitan profile scope for character dictionary settings updates. Values: all | active
|
||||||
|
"collapsibleSections": {
|
||||||
|
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
||||||
|
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
||||||
|
"voicedBy": false // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
|
||||||
|
} // Collapsible sections setting.
|
||||||
|
} // Character dictionary setting.
|
||||||
}, // Anilist API credentials and update behavior.
|
}, // Anilist API credentials and update behavior.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Yomitan
|
||||||
|
// Optional external Yomitan profile integration.
|
||||||
|
// Setting yomitan.externalProfilePath switches SubMiner to read-only external-profile mode.
|
||||||
|
// For GameSentenceMiner on Linux, the default overlay profile is usually ~/.config/gsm_overlay.
|
||||||
|
// In external-profile mode SubMiner will not import, delete, or modify Yomitan dictionaries/settings.
|
||||||
|
// ==========================================
|
||||||
|
"yomitan": {
|
||||||
|
"externalProfilePath": "" // Optional external Yomitan Electron profile path to use in read-only mode for shared dictionaries/settings. Example: ~/.config/gsm_overlay
|
||||||
|
}, // Optional external Yomitan profile integration.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// MPV Launcher
|
||||||
|
// SubMiner-managed mpv launch and bundled plugin options.
|
||||||
|
// Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.
|
||||||
|
// autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.
|
||||||
|
// Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.
|
||||||
|
// Set mpv.profile to pass an mpv profile to managed mpv launches; leave it blank to pass none.
|
||||||
|
// Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.
|
||||||
|
// ==========================================
|
||||||
|
"mpv": {
|
||||||
|
"executablePath": "", // Optional absolute path to mpv.exe for Windows launch flows. Leave empty to auto-discover from SUBMINER_MPV_PATH or PATH.
|
||||||
|
"launchMode": "normal", // Default window state for SubMiner-managed mpv launches. Values: normal | maximized | fullscreen
|
||||||
|
"profile": "", // Optional mpv profile name passed to SubMiner-managed mpv launches. Leave empty to pass no profile.
|
||||||
|
"socketPath": "/tmp/subminer-socket", // mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.
|
||||||
|
"backend": "auto", // Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform. Values: auto | hyprland | sway | x11 | macos | windows
|
||||||
|
"autoStartSubMiner": true, // Start SubMiner in the background when SubMiner-managed mpv loads a file. Values: true | false
|
||||||
|
"pauseUntilOverlayReady": true, // Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness. Values: true | false
|
||||||
|
"subminerBinaryPath": "", // Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.
|
||||||
|
"aniskipEnabled": true, // Enable AniSkip intro detection, chapter markers, and the skip-intro key. Values: true | false
|
||||||
|
"aniskipButtonKey": "TAB" // mpv key used to skip the detected intro while the skip prompt is visible.
|
||||||
|
}, // SubMiner-managed mpv launch and bundled plugin options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Jellyfin
|
// Jellyfin
|
||||||
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
// Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
@@ -273,20 +758,25 @@
|
|||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"enabled": false, // Enable optional Jellyfin integration and CLI control commands. Values: true | false
|
"enabled": false, // Enable optional Jellyfin integration and CLI control commands. Values: true | false
|
||||||
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
"serverUrl": "", // Base Jellyfin server URL (for example: http://localhost:8096).
|
||||||
|
"recentServers": [], // Recently authenticated Jellyfin server URLs shown in setup.
|
||||||
"username": "", // Default Jellyfin username used during CLI login.
|
"username": "", // Default Jellyfin username used during CLI login.
|
||||||
"deviceId": "subminer", // Device id setting.
|
|
||||||
"clientName": "SubMiner", // Client name setting.
|
|
||||||
"clientVersion": "0.1.0", // Client version setting.
|
|
||||||
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
|
"defaultLibraryId": "", // Optional default Jellyfin library ID for item listing.
|
||||||
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
|
"remoteControlEnabled": true, // Enable Jellyfin remote cast control mode. Values: true | false
|
||||||
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
|
"remoteControlAutoConnect": true, // Auto-connect to the configured remote control target. Values: true | false
|
||||||
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
|
"autoAnnounce": false, // When enabled, automatically trigger remote announce/visibility check on websocket connect. Values: true | false
|
||||||
"remoteControlDeviceName": "SubMiner", // Device name reported for Jellyfin remote control sessions.
|
|
||||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||||
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
|
"directPlayContainers": [
|
||||||
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
|
"mkv",
|
||||||
|
"mp4",
|
||||||
|
"webm",
|
||||||
|
"mov",
|
||||||
|
"flac",
|
||||||
|
"mp3",
|
||||||
|
"aac"
|
||||||
|
], // Container allowlist for direct play decisions.
|
||||||
|
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
|
||||||
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -295,9 +785,10 @@
|
|||||||
// Uses official SubMiner Discord app assets for polished card visuals.
|
// Uses official SubMiner Discord app assets for polished card visuals.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"discordPresence": {
|
"discordPresence": {
|
||||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
"enabled": true, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||||
|
"presenceStyle": "default", // Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal". Values: default | meme | japanese | minimal
|
||||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||||
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
|
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -314,12 +805,33 @@
|
|||||||
"queueCap": 1000, // In-memory write queue cap before overflow policy applies.
|
"queueCap": 1000, // In-memory write queue cap before overflow policy applies.
|
||||||
"payloadCapBytes": 256, // Max JSON payload size per event before truncation.
|
"payloadCapBytes": 256, // Max JSON payload size per event before truncation.
|
||||||
"maintenanceIntervalMs": 86400000, // Maintenance cadence (prune + rollup + vacuum checks).
|
"maintenanceIntervalMs": 86400000, // Maintenance cadence (prune + rollup + vacuum checks).
|
||||||
|
"retentionMode": "preset", // Retention mode (`preset` uses preset values, `advanced` uses explicit values). Values: preset | advanced
|
||||||
|
"retentionPreset": "balanced", // Retention preset when `retentionMode` is `preset`. Values: minimal | balanced | deep-history
|
||||||
"retention": {
|
"retention": {
|
||||||
"eventsDays": 7, // Raw event retention window in days.
|
"eventsDays": 0, // Raw event retention window in days. Use 0 to keep all.
|
||||||
"telemetryDays": 30, // Telemetry retention window in days.
|
"telemetryDays": 0, // Telemetry retention window in days. Use 0 to keep all.
|
||||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
"sessionsDays": 0, // Session retention window in days. Use 0 to keep all.
|
||||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
"dailyRollupsDays": 0, // Daily rollup retention window in days. Use 0 to keep all.
|
||||||
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
|
"monthlyRollupsDays": 0, // Monthly rollup retention window in days. Use 0 to keep all.
|
||||||
|
"vacuumIntervalDays": 0 // Minimum days between VACUUM runs. Use 0 to disable.
|
||||||
}, // Retention setting.
|
}, // Retention setting.
|
||||||
|
"lifetimeSummaries": {
|
||||||
|
"global": true, // Maintain global lifetime stats rows. Values: true | false
|
||||||
|
"anime": true, // Maintain per-anime lifetime stats rows. Values: true | false
|
||||||
|
"media": true // Maintain per-media lifetime stats rows. Values: true | false
|
||||||
|
} // Lifetime summaries setting.
|
||||||
}, // Enable/disable immersion tracking.
|
}, // Enable/disable immersion tracking.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Stats Dashboard
|
||||||
|
// Local immersion stats dashboard served on localhost and available as an in-app overlay.
|
||||||
|
// Uses the immersion tracking database for overview, trends, sessions, and vocabulary views.
|
||||||
|
// ==========================================
|
||||||
|
"stats": {
|
||||||
|
"toggleKey": "Backquote", // Key code to toggle the stats overlay.
|
||||||
|
"markWatchedKey": "KeyW", // Key code to mark the current video as watched and advance to the next playlist entry.
|
||||||
|
"serverPort": 6969, // Port for the stats HTTP server.
|
||||||
|
"autoStartServer": true, // Automatically start the stats server on launch. Values: true | false
|
||||||
|
"autoOpenBrowser": false // Automatically open the stats dashboard in a browser when the server starts. Values: true | false
|
||||||
|
} // Local immersion stats dashboard served on localhost and available as an in-app overlay.
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
.vitepress/cache/
|
||||||
|
.vitepress/dist/
|
||||||
|
.DS_Store
|
||||||
|
.codex/
|
||||||
|
.agents/
|
||||||
|
**/CLAUDE.md
|
||||||
|
.claude/*
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import type { DefaultTheme, HeadConfig, TransformContext, UserConfig } from 'vitepress';
|
||||||
|
|
||||||
|
const DOCS_HOSTNAME = 'https://docs.subminer.moe';
|
||||||
|
const PLAUSIBLE_PROXY_HOSTNAME = 'https://worker.sudacode.com';
|
||||||
|
const PLAUSIBLE_SITE_SCRIPT_PATH = '/js/pa-h28Pn9ppgTJRmiSJlyPT6.js';
|
||||||
|
const PLAUSIBLE_ENDPOINT = `${PLAUSIBLE_PROXY_HOSTNAME}/api/event`;
|
||||||
|
const PLAUSIBLE_INIT_SCRIPT = [
|
||||||
|
'window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};',
|
||||||
|
`plausible.init({ endpoint: '${PLAUSIBLE_ENDPOINT}' });`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
type DocsChannel = 'stable-root' | 'stable-archive' | 'main';
|
||||||
|
|
||||||
|
function optionalEnv(value: string | undefined): string | undefined {
|
||||||
|
return value && value !== 'undefined' ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = normalizeBase(optionalEnv(process.env.SUBMINER_DOCS_BASE) ?? '/');
|
||||||
|
const outDir = optionalEnv(process.env.SUBMINER_DOCS_OUT_DIR);
|
||||||
|
const docsSourceDir = optionalEnv(process.env.SUBMINER_DOCS_SOURCE_DIR) ?? process.cwd();
|
||||||
|
// The tracked `docs-site/` checkout, which stays a git working tree even when
|
||||||
|
// `docsSourceDir` points at an untracked release snapshot. Used for git lookups only.
|
||||||
|
const repoDocsDir = optionalEnv(process.env.SUBMINER_DOCS_REPO_DIR) ?? process.cwd();
|
||||||
|
const channel = normalizeChannel(optionalEnv(process.env.SUBMINER_DOCS_CHANNEL));
|
||||||
|
const docsVersion = optionalEnv(process.env.SUBMINER_DOCS_VERSION);
|
||||||
|
|
||||||
|
function normalizeBase(value: string): string {
|
||||||
|
if (!value || value === '/') return '/';
|
||||||
|
return `/${value.replace(/^\/+|\/+$/g, '')}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeChannel(value: string | undefined): DocsChannel {
|
||||||
|
if (value === 'main' || value === 'stable-archive') return value;
|
||||||
|
return 'stable-root';
|
||||||
|
}
|
||||||
|
|
||||||
|
function withDocsBase(path: string): string {
|
||||||
|
if (/^[a-z]+:\/\//i.test(path)) return path;
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
if (base === '/') return normalizedPath;
|
||||||
|
return `${base.replace(/\/$/, '')}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageToRoute(page: string): string | null {
|
||||||
|
if (page === '404.md') return null;
|
||||||
|
|
||||||
|
const route = page
|
||||||
|
.replace(/(^|\/)index\.md$/, '')
|
||||||
|
.replace(/\.md$/, '')
|
||||||
|
.replace(/\/$/, '');
|
||||||
|
return route ? `/${route}` : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the root channel is indexable. `main` and every /v/<version>/ archive are
|
||||||
|
// near-verbatim copies of it, so they own their URL via a self-referential canonical
|
||||||
|
// and are excluded from the index instead of being consolidated onto root. Uniform
|
||||||
|
// self-canonical plus noindex avoids mixing noindex with a cross-page canonical,
|
||||||
|
// which Google treats as a conflicting signal.
|
||||||
|
const isIndexableChannel = channel === 'stable-root';
|
||||||
|
|
||||||
|
function pageToCanonicalHref(page: string): string | null {
|
||||||
|
const route = pageToRoute(page);
|
||||||
|
if (!route) return null;
|
||||||
|
|
||||||
|
if (!isIndexableChannel) {
|
||||||
|
return `${DOCS_HOSTNAME}${canonicalRouteWithBase(route)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return route === '/' ? `${DOCS_HOSTNAME}/` : `${DOCS_HOSTNAME}${route}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalRouteWithBase(route: string): string {
|
||||||
|
const routeWithBase = withDocsBase(route);
|
||||||
|
return route === '/' ? routeWithBase : routeWithBase.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPageHead({ page }: TransformContext): HeadConfig[] {
|
||||||
|
const href = pageToCanonicalHref(page);
|
||||||
|
const head: HeadConfig[] = href ? [['link', { rel: 'canonical', href }]] : [];
|
||||||
|
|
||||||
|
// Crawlable so links still pass through, but out of the index: ~30 archived copies
|
||||||
|
// of every page otherwise soak up the crawl budget the current docs need.
|
||||||
|
if (!isIndexableChannel) {
|
||||||
|
head.push(['meta', { name: 'robots', content: 'noindex,follow' }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkToPagePath(link: string): string | null {
|
||||||
|
if (!link.startsWith('/') || link.startsWith('/v/') || link.startsWith('/main/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutHash = link.split('#')[0] ?? '/';
|
||||||
|
const withoutQuery = withoutHash.split('?')[0] ?? '/';
|
||||||
|
const route = withoutQuery.replace(/^\/+|\/+$/g, '');
|
||||||
|
return route ? `${route}.md` : 'index.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPageForLink(link: string): boolean {
|
||||||
|
const pagePath = linkToPagePath(link);
|
||||||
|
if (!pagePath) return true;
|
||||||
|
return existsSync(join(docsSourceDir, pagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNav(items: DefaultTheme.NavItem[]): DefaultTheme.NavItem[] {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
if ('items' in item && item.items) {
|
||||||
|
return { ...item, items: filterNav(item.items as DefaultTheme.NavItem[]) };
|
||||||
|
}
|
||||||
|
if ('link' in item && item.link && !hasPageForLink(item.link)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.filter((item): item is DefaultTheme.NavItem => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
const filteredChildren = item.items ? filterSidebar(item.items) : undefined;
|
||||||
|
if (item.link && !hasPageForLink(item.link)) return null;
|
||||||
|
if (item.items && filteredChildren?.length === 0 && !item.link) return null;
|
||||||
|
return { ...item, items: filteredChildren };
|
||||||
|
})
|
||||||
|
.filter((item): item is DefaultTheme.SidebarItem => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version navigation targets other builds (root, `/main/`, `/versions`), so it links to
|
||||||
|
// production by absolute URL: base-relative links would stay inside this build, and
|
||||||
|
// `target: '_self'` makes the VitePress router do a full page load. The list is
|
||||||
|
// deliberately static; the full release list lives on the root-only `/versions` page
|
||||||
|
// so frozen `/v/<version>/` archives never need a rebuild when a new tag ships.
|
||||||
|
const versionItems: DefaultTheme.NavItemWithLink[] = [
|
||||||
|
{ text: 'Latest stable', link: `${DOCS_HOSTNAME}/` },
|
||||||
|
{ text: 'main', link: `${DOCS_HOSTNAME}/main/` },
|
||||||
|
{ text: 'All versions', link: `${DOCS_HOSTNAME}/versions` },
|
||||||
|
].map((item) => ({ ...item, target: '_self', noIcon: true }));
|
||||||
|
|
||||||
|
function sitemapUrlToPage(url: string): string {
|
||||||
|
const route = url.replace(/\.html$/, '').replace(/^\/+|\/+$/g, '');
|
||||||
|
return route ? `${route}.md` : 'index.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
// VitePress derives <lastmod> by running `git log` inside its source dir. Production
|
||||||
|
// builds point that at an untracked snapshot of the release tag, so the lookup comes
|
||||||
|
// back empty and the sitemap ships with no dates at all. Resolve it from the tracked
|
||||||
|
// checkout at the ref being built instead.
|
||||||
|
function lastModifiedFor(url: string): string | undefined {
|
||||||
|
const ref = docsVersion && docsVersion !== 'main' ? docsVersion : 'HEAD';
|
||||||
|
const result = spawnSync('git', ['log', '-1', '--format=%cI', ref, '--', sitemapUrlToPage(url)], {
|
||||||
|
cwd: repoDocsDir,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (result.status === 0 && result.stdout.trim()) || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the root channel publishes a sitemap. Archived and `main` builds would emit
|
||||||
|
// their own copies listing the same canonical URLs, which just advertises the
|
||||||
|
// duplicate trees we are trying to keep out of the index.
|
||||||
|
const sitemap: UserConfig['sitemap'] = isIndexableChannel
|
||||||
|
? {
|
||||||
|
hostname: DOCS_HOSTNAME,
|
||||||
|
transformItems(items) {
|
||||||
|
return items
|
||||||
|
.filter((item) => item.url !== 'README' && item.url !== `${DOCS_HOSTNAME}/README`)
|
||||||
|
.map((item) => ({ ...item, lastmod: item.lastmod ?? lastModifiedFor(item.url) }));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const nav: DefaultTheme.NavItem[] = [
|
||||||
|
{ text: 'Home', link: '/' },
|
||||||
|
{ text: 'Get Started', link: '/installation' },
|
||||||
|
{ text: 'Mining', link: '/mining-workflow' },
|
||||||
|
{ text: 'Configuration', link: '/configuration' },
|
||||||
|
{ text: 'Changelog', link: '/changelog' },
|
||||||
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||||
|
{ text: docsVersion ?? 'main', items: versionItems },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sidebar: DefaultTheme.SidebarItem[] = [
|
||||||
|
{
|
||||||
|
text: 'Getting Started',
|
||||||
|
items: [
|
||||||
|
{ text: 'Overview', link: '/' },
|
||||||
|
{ text: 'Installation', link: '/installation' },
|
||||||
|
{ text: 'Usage', link: '/usage' },
|
||||||
|
{ text: 'Mining Workflow', link: '/mining-workflow' },
|
||||||
|
{ text: 'Launcher Script', link: '/launcher-script' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Reference',
|
||||||
|
items: [
|
||||||
|
{ text: 'Configuration', link: '/configuration' },
|
||||||
|
{ text: 'Keyboard Shortcuts', link: '/shortcuts' },
|
||||||
|
{ text: 'Subtitle Annotations', link: '/subtitle-annotations' },
|
||||||
|
{ text: 'Subtitle Sidebar', link: '/subtitle-sidebar' },
|
||||||
|
{ text: 'Immersion Tracking', link: '/immersion-tracking' },
|
||||||
|
{ text: 'Troubleshooting', link: '/troubleshooting' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Integrations',
|
||||||
|
items: [
|
||||||
|
{ text: 'MPV Plugin', link: '/mpv-plugin' },
|
||||||
|
{ text: 'Anki', link: '/anki-integration' },
|
||||||
|
{ text: 'Jellyfin', link: '/jellyfin-integration' },
|
||||||
|
{ text: 'YouTube', link: '/youtube-integration' },
|
||||||
|
{ text: 'Jimaku', link: '/jimaku-integration' },
|
||||||
|
{ text: 'Subtitle Generation', link: '/subtitle-generation' },
|
||||||
|
{ text: 'TsukiHime', link: '/tsukihime-integration' },
|
||||||
|
{ text: 'AniList', link: '/anilist-integration' },
|
||||||
|
{ text: 'AniSkip', link: '/aniskip-integration' },
|
||||||
|
{ text: 'Character Dictionary', link: '/character-dictionary' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Development',
|
||||||
|
items: [
|
||||||
|
{ text: 'Building & Testing', link: '/development' },
|
||||||
|
{ text: 'Architecture', link: '/architecture' },
|
||||||
|
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
|
||||||
|
{ text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
|
||||||
|
{ text: 'Changelog', link: '/changelog' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const config: UserConfig = {
|
||||||
|
title: 'SubMiner Docs',
|
||||||
|
description:
|
||||||
|
'SubMiner: an MPV immersion-mining overlay with Yomitan and AnkiConnect integration.',
|
||||||
|
base,
|
||||||
|
...(outDir ? { outDir } : {}),
|
||||||
|
head: [
|
||||||
|
['link', { rel: 'preconnect', href: PLAUSIBLE_PROXY_HOSTNAME }],
|
||||||
|
[
|
||||||
|
'script',
|
||||||
|
{
|
||||||
|
async: '',
|
||||||
|
src: `${PLAUSIBLE_PROXY_HOSTNAME}${PLAUSIBLE_SITE_SCRIPT_PATH}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['script', {}, PLAUSIBLE_INIT_SCRIPT],
|
||||||
|
['link', { rel: 'icon', href: withDocsBase('/favicon.ico'), sizes: 'any' }],
|
||||||
|
[
|
||||||
|
'link',
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/png',
|
||||||
|
href: withDocsBase('/favicon-32x32.png'),
|
||||||
|
sizes: '32x32',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'link',
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/png',
|
||||||
|
href: withDocsBase('/favicon-16x16.png'),
|
||||||
|
sizes: '16x16',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'link',
|
||||||
|
{
|
||||||
|
rel: 'apple-touch-icon',
|
||||||
|
href: withDocsBase('/apple-touch-icon.png'),
|
||||||
|
sizes: '180x180',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
appearance: 'dark',
|
||||||
|
cleanUrls: true,
|
||||||
|
metaChunk: true,
|
||||||
|
sitemap,
|
||||||
|
transformHead: transformPageHead,
|
||||||
|
lastUpdated: true,
|
||||||
|
srcExclude: ['subagents/**', 'README.md'],
|
||||||
|
markdown: {
|
||||||
|
theme: {
|
||||||
|
light: 'catppuccin-latte',
|
||||||
|
dark: 'catppuccin-macchiato',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
themeConfig: {
|
||||||
|
logo: {
|
||||||
|
light: '/assets/SubMiner.png',
|
||||||
|
dark: '/assets/SubMiner.png',
|
||||||
|
},
|
||||||
|
siteTitle: 'SubMiner Docs',
|
||||||
|
nav: filterNav(nav),
|
||||||
|
sidebar: filterSidebar(sidebar),
|
||||||
|
search: {
|
||||||
|
provider: 'local',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
message: 'Released under the GPL-3.0 License.',
|
||||||
|
copyright: 'Copyright © 2026-present sudacode',
|
||||||
|
},
|
||||||
|
editLink: {
|
||||||
|
pattern: 'https://github.com/ksyasuda/SubMiner/edit/main/docs-site/:path',
|
||||||
|
text: 'Edit this page on GitHub',
|
||||||
|
},
|
||||||
|
outline: { level: [2, 3], label: 'On this page' },
|
||||||
|
externalLinkIcon: true,
|
||||||
|
docFooter: { prev: 'Previous', next: 'Next' },
|
||||||
|
returnToTopLabel: 'Back to top',
|
||||||
|
socialLinks: [{ icon: 'github', link: 'https://github.com/ksyasuda/SubMiner' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
import DefaultTheme from 'vitepress/theme';
|
||||||
|
import StatusLine from './components/StatusLine.vue';
|
||||||
|
import BlinkingCursor from './components/BlinkingCursor.vue';
|
||||||
|
|
||||||
|
const { Layout } = DefaultTheme;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
<template #home-hero-info-after>
|
||||||
|
<BlinkingCursor />
|
||||||
|
</template>
|
||||||
|
<template #layout-bottom>
|
||||||
|
<StatusLine />
|
||||||
|
</template>
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<span class="tui-cursor" aria-hidden="true">█</span>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRoute, useData } from 'vitepress';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { formatStatusLineDate, formatStatusLineFilePath } from '../status-line';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const { frontmatter } = useData();
|
||||||
|
|
||||||
|
const mode = computed(() => {
|
||||||
|
const layout = frontmatter.value.layout;
|
||||||
|
if (layout === 'home') return 'HOME';
|
||||||
|
return 'NORMAL';
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = computed(() => {
|
||||||
|
return formatStatusLineFilePath(route.path);
|
||||||
|
});
|
||||||
|
|
||||||
|
const section = computed(() => {
|
||||||
|
const path = route.path;
|
||||||
|
if (path === '/') return 'root';
|
||||||
|
const parts = path.split('/').filter(Boolean);
|
||||||
|
return parts[0] || 'root';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set on the client only, so the prerendered HTML never bakes in the build date.
|
||||||
|
const today = ref('');
|
||||||
|
onMounted(() => {
|
||||||
|
today.value = formatStatusLineDate(new Date());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="tui-statusline" aria-label="Page info">
|
||||||
|
<div class="tui-statusline__left">
|
||||||
|
<span class="tui-statusline__mode" :data-mode="mode">{{ mode }}</span>
|
||||||
|
<span class="tui-statusline__sep"></span>
|
||||||
|
<span class="tui-statusline__file">{{ filePath }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tui-statusline__right">
|
||||||
|
<span class="tui-statusline__section">{{ section }}</span>
|
||||||
|
<span class="tui-statusline__sep"></span>
|
||||||
|
<span v-if="today" class="tui-statusline__date">{{ today }}</span>
|
||||||
|
<span v-if="today" class="tui-statusline__sep"></span>
|
||||||
|
<span class="tui-statusline__branch">GPL-3.0</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
@@ -2,7 +2,9 @@ import DefaultTheme from 'vitepress/theme';
|
|||||||
import { useRoute } from 'vitepress';
|
import { useRoute } from 'vitepress';
|
||||||
import { nextTick, onMounted, watch } from 'vue';
|
import { nextTick, onMounted, watch } from 'vue';
|
||||||
import '@catppuccin/vitepress/theme/macchiato/mauve.css';
|
import '@catppuccin/vitepress/theme/macchiato/mauve.css';
|
||||||
|
import './tui-theme.css';
|
||||||
import './mermaid-modal.css';
|
import './mermaid-modal.css';
|
||||||
|
import TuiLayout from './TuiLayout.vue';
|
||||||
|
|
||||||
let mermaidLoader: Promise<any> | null = null;
|
let mermaidLoader: Promise<any> | null = null;
|
||||||
const MERMAID_MODAL_ID = 'mermaid-diagram-modal';
|
const MERMAID_MODAL_ID = 'mermaid-diagram-modal';
|
||||||
@@ -112,6 +114,11 @@ async function getMermaid() {
|
|||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
securityLevel: 'loose',
|
securityLevel: 'loose',
|
||||||
theme: 'base',
|
theme: 'base',
|
||||||
|
flowchart: {
|
||||||
|
padding: 16,
|
||||||
|
nodeSpacing: 30,
|
||||||
|
rankSpacing: 40,
|
||||||
|
},
|
||||||
themeVariables: {
|
themeVariables: {
|
||||||
background: '#24273a',
|
background: '#24273a',
|
||||||
primaryColor: '#363a4f',
|
primaryColor: '#363a4f',
|
||||||
@@ -177,7 +184,8 @@ async function renderMermaidBlocks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...DefaultTheme,
|
Layout: TuiLayout,
|
||||||
|
extends: DefaultTheme,
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const render = () => {
|
const render = () => {
|
||||||
@@ -188,7 +196,9 @@ export default {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(render);
|
onMounted(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
watch(() => route.path, render);
|
watch(() => route.path, render);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
.mermaid-interactive {
|
.mermaid-interactive {
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
|
transition: outline-color 180ms ease;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-interactive svg {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mermaid-interactive:focus-visible {
|
.mermaid-interactive:focus-visible {
|
||||||
outline: 2px solid var(--vp-c-brand-1);
|
outline: 2px solid var(--vp-c-brand-1);
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mermaid-modal {
|
.mermaid-modal {
|
||||||
@@ -23,6 +32,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.72);
|
background: rgba(0, 0, 0, 0.72);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mermaid-modal__dialog {
|
.mermaid-modal__dialog {
|
||||||
@@ -32,9 +43,9 @@
|
|||||||
width: min(96vw, 1800px);
|
width: min(96vw, 1800px);
|
||||||
max-height: 92vh;
|
max-height: 92vh;
|
||||||
border: 1px solid var(--vp-c-border);
|
border: 1px solid var(--vp-c-border);
|
||||||
border-radius: 12px;
|
border-radius: 0;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
box-shadow: var(--vp-shadow-4);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 24px 64px rgba(0, 0, 0, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +55,19 @@
|
|||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
border: 1px solid var(--vp-c-border);
|
border: 1px solid var(--vp-c-border);
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
font-size: 14px;
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 180ms ease, color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-modal__close:hover {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mermaid-modal__content {
|
.mermaid-modal__content {
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { expect, test } from 'bun:test';
|
||||||
|
import { formatStatusLineDate, formatStatusLineFilePath } from './status-line';
|
||||||
|
|
||||||
|
test('status line file path formats root home as index markdown', () => {
|
||||||
|
expect(formatStatusLineFilePath('/')).toBe('index.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('status line file path formats version archive home without trailing slash', () => {
|
||||||
|
expect(formatStatusLineFilePath('/v/0.12.0/')).toBe('v/0.12.0.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('status line file path keeps normal docs routes as markdown files', () => {
|
||||||
|
expect(formatStatusLineFilePath('/v/0.12.0/configuration')).toBe('v/0.12.0/configuration.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('status line date uses the local calendar day, zero padded', () => {
|
||||||
|
expect(formatStatusLineDate(new Date(2026, 0, 5, 23, 59))).toBe('2026-01-05');
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function formatStatusLineFilePath(routePath: string): string {
|
||||||
|
if (routePath === '/') return 'index.md';
|
||||||
|
return `${routePath.replace(/^\/|\/$/g, '')}.md`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local calendar date as YYYY-MM-DD (toISOString would give the UTC date).
|
||||||
|
export function formatStatusLineDate(date: Date): string {
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${date.getFullYear()}-${month}-${day}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,689 @@
|
|||||||
|
@import '@fontsource/jetbrains-mono/400.css';
|
||||||
|
@import '@fontsource/jetbrains-mono/500.css';
|
||||||
|
@import '@fontsource/jetbrains-mono/600.css';
|
||||||
|
@import '@fontsource/jetbrains-mono/700.css';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'M PLUS 1';
|
||||||
|
src: url('../../public/assets/fonts/Mplus1-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope Default';
|
||||||
|
src: url('../../public/assets/fonts/manrope-latin-600-normal.ttf') format('truetype');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--tui-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||||
|
--tui-font-body:
|
||||||
|
'Manrope Default',
|
||||||
|
'M PLUS 1',
|
||||||
|
'Manrope',
|
||||||
|
'Noto Sans CJK JP',
|
||||||
|
'Noto Sans JP',
|
||||||
|
'Hiragino Kaku Gothic ProN',
|
||||||
|
'Meiryo',
|
||||||
|
'Yu Gothic',
|
||||||
|
'Hiragino Sans',
|
||||||
|
system-ui,
|
||||||
|
sans-serif;
|
||||||
|
--tui-transition: 180ms ease;
|
||||||
|
|
||||||
|
/* Theme-specific values — overridden in .dark below */
|
||||||
|
--tui-nav-bg: color-mix(in srgb, var(--vp-c-bg-alt) 88%, transparent);
|
||||||
|
--tui-table-hover-bg: color-mix(in srgb, var(--vp-c-bg-soft) 80%, transparent);
|
||||||
|
--tui-link-underline: color-mix(in srgb, var(--vp-c-brand-1) 40%, transparent);
|
||||||
|
--tui-selection-bg: hsla(267, 83%, 45%, 0.14);
|
||||||
|
--tui-hero-glow: hsla(267, 83%, 45%, 0.05);
|
||||||
|
--tui-step-hover-bg: var(--vp-c-bg-alt);
|
||||||
|
--tui-step-hover-glow: color-mix(in srgb, var(--vp-c-brand-1) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--tui-nav-bg: hsla(232, 23%, 18%, 0.82);
|
||||||
|
--tui-table-hover-bg: hsla(232, 23%, 18%, 0.4);
|
||||||
|
--tui-link-underline: hsla(267, 83%, 80%, 0.3);
|
||||||
|
--tui-selection-bg: hsla(267, 83%, 80%, 0.22);
|
||||||
|
--tui-hero-glow: hsla(267, 83%, 80%, 0.06);
|
||||||
|
--tui-step-hover-bg: hsla(232, 23%, 18%, 0.6);
|
||||||
|
--tui-step-hover-glow: hsla(267, 83%, 80%, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-font-family-base: var(--tui-font-body);
|
||||||
|
--vp-font-family-heading: var(--tui-font-body);
|
||||||
|
--vp-font-family-mono: var(--tui-font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc {
|
||||||
|
font-family: var(--vp-font-family-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Selection === */
|
||||||
|
::selection {
|
||||||
|
background: var(--tui-selection-bg);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Scrollbar === */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--vp-c-divider);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Global transitions === */
|
||||||
|
a,
|
||||||
|
button,
|
||||||
|
.VPFeature,
|
||||||
|
.VPNavBarMenuLink,
|
||||||
|
.VPSidebarItem .text {
|
||||||
|
transition: color var(--tui-transition), background var(--tui-transition),
|
||||||
|
border-color var(--tui-transition), opacity var(--tui-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Nav bar === */
|
||||||
|
.VPNav .VPNavBarTitle .title {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBarMenuLink {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar {
|
||||||
|
border-bottom: 1px solid var(--vp-c-divider);
|
||||||
|
backdrop-filter: blur(12px) saturate(1.2);
|
||||||
|
-webkit-backdrop-filter: blur(12px) saturate(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar:not(.has-sidebar) {
|
||||||
|
background: var(--tui-nav-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPNav .VPNavBar.has-sidebar .content {
|
||||||
|
backdrop-filter: blur(12px) saturate(1.2);
|
||||||
|
-webkit-backdrop-filter: blur(12px) saturate(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Sidebar === */
|
||||||
|
.VPSidebar .VPSidebarItem .text {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar .VPSidebarItem.is-active.is-link > .item .text::before {
|
||||||
|
content: '> ';
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar .VPSidebarItem.is-link:not(.is-active) > .item .text::before {
|
||||||
|
content: ' ';
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar .VPSidebarItem.is-link:not(.is-active) > .item:hover .text::before {
|
||||||
|
content: '> ';
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar .VPSidebarItem.is-link:not(.is-active) > .item:hover .text {
|
||||||
|
color: var(--vp-c-text-1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar .VPSidebarItem.level-0 > .item .text {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar .VPSidebarItem.level-0 > .item .text::before {
|
||||||
|
content: '# ';
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPSidebar .VPSidebarItem.level-0 .items {
|
||||||
|
border-left: 1px solid var(--vp-c-divider);
|
||||||
|
margin-left: 6px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Headings === */
|
||||||
|
.vp-doc h1,
|
||||||
|
.vp-doc h2,
|
||||||
|
.vp-doc h3,
|
||||||
|
.vp-doc h4 {
|
||||||
|
font-family: var(--vp-font-family-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h1 {
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h1::before {
|
||||||
|
content: '> ';
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h2 {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc h2::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
height: 1px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--vp-c-divider) 0,
|
||||||
|
var(--vp-c-divider) 1ch,
|
||||||
|
transparent 1ch,
|
||||||
|
transparent 1.5ch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Inline code === */
|
||||||
|
.vp-doc :not(pre) > code {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.88em;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP',
|
||||||
|
monospace;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Code blocks === */
|
||||||
|
.vp-doc div[class*='language-'] {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
background: var(--vp-c-bg-alt) !important;
|
||||||
|
font-family: var(--tui-font-mono), 'M PLUS 1', 'Noto Sans CJK JP', 'Noto Sans JP',
|
||||||
|
monospace;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc div[class*='language-']::before {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc div[class*='language-'] > button.copy {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Tables === */
|
||||||
|
.vp-doc table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc table th {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc table td,
|
||||||
|
.vp-doc table th {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc table tr:hover td {
|
||||||
|
background: var(--tui-table-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Links === */
|
||||||
|
.vp-doc a {
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--tui-link-underline);
|
||||||
|
transition: border-color var(--tui-transition), color var(--tui-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a:hover {
|
||||||
|
border-bottom-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Custom containers === */
|
||||||
|
.vp-doc .custom-block {
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin: 16px 0;
|
||||||
|
position: relative;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .custom-block::before {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: 12px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 0 6px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .custom-block.tip,
|
||||||
|
.vp-doc .custom-block.info,
|
||||||
|
.vp-doc .custom-block.warning,
|
||||||
|
.vp-doc .custom-block.danger,
|
||||||
|
.vp-doc .custom-block.details {
|
||||||
|
border-left-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc .custom-block.tip { border-color: var(--vp-c-brand-1); }
|
||||||
|
.vp-doc .custom-block.tip::before { content: '-- tip'; color: var(--vp-c-brand-1); }
|
||||||
|
|
||||||
|
.vp-doc .custom-block.info { border-color: var(--vp-c-brand-2); }
|
||||||
|
.vp-doc .custom-block.info::before { content: '-- info'; color: var(--vp-c-brand-2); }
|
||||||
|
|
||||||
|
.vp-doc .custom-block.warning { border-color: var(--vp-c-warning-1); }
|
||||||
|
.vp-doc .custom-block.warning::before { content: '-- warning'; color: var(--vp-c-warning-1); }
|
||||||
|
|
||||||
|
.vp-doc .custom-block.danger { border-color: var(--vp-c-danger-1); }
|
||||||
|
.vp-doc .custom-block.danger::before { content: '-- danger'; color: var(--vp-c-danger-1); }
|
||||||
|
|
||||||
|
.vp-doc .custom-block.details { border-color: var(--vp-c-divider); }
|
||||||
|
.vp-doc .custom-block.details::before { content: '-- details'; color: var(--vp-c-text-2); }
|
||||||
|
|
||||||
|
.vp-doc .custom-block .custom-block-title {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Blockquotes === */
|
||||||
|
.vp-doc blockquote {
|
||||||
|
border-left: 2px solid var(--vp-c-divider);
|
||||||
|
padding: 4px 0 4px 16px;
|
||||||
|
margin: 16px 0;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-style: normal;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc blockquote::before {
|
||||||
|
content: '│';
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
top: 4px;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Horizontal rules === */
|
||||||
|
.vp-doc hr {
|
||||||
|
border: none;
|
||||||
|
height: 1px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--vp-c-divider) 0,
|
||||||
|
var(--vp-c-divider) 1ch,
|
||||||
|
transparent 1ch,
|
||||||
|
transparent 1.5ch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Lists === */
|
||||||
|
.vp-doc ul > li::marker {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc ol > li::marker {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Focus-visible === */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--vp-c-brand-1);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-doc a:focus-visible {
|
||||||
|
outline-offset: 3px;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Feature icon hover === */
|
||||||
|
.VPFeatures .VPFeature .icon {
|
||||||
|
transition: transform 280ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFeatures .VPFeature:hover .icon {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Blinking cursor === */
|
||||||
|
.tui-cursor {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
animation: tui-blink 1s step-end infinite;
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 0.9em;
|
||||||
|
vertical-align: baseline;
|
||||||
|
margin-left: 2px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tui-blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* === Statusline === */
|
||||||
|
.tui-statusline {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__left,
|
||||||
|
.tui-statusline__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__mode {
|
||||||
|
padding: 0 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--vp-c-bg);
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
line-height: 28px;
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__mode[data-mode="HOME"] {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__sep {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
color: var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__sep::after {
|
||||||
|
content: '|';
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__file {
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__section {
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__date {
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tui-statusline__branch {
|
||||||
|
padding: 0 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-bg);
|
||||||
|
background: var(--vp-c-brand-3, var(--vp-c-brand-1));
|
||||||
|
line-height: 28px;
|
||||||
|
margin-right: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.tui-statusline__section,
|
||||||
|
.tui-statusline__date {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHome .VPHero::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Hero === */
|
||||||
|
.VPHero .name {
|
||||||
|
font-family: var(--tui-font-mono) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .clip {
|
||||||
|
-webkit-text-fill-color: var(--vp-c-brand-1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .text {
|
||||||
|
font-family: var(--tui-font-mono) !important;
|
||||||
|
font-size: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .tagline {
|
||||||
|
font-family: var(--tui-font-body);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .actions .action .VPButton {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
transition: all var(--tui-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .actions .action .VPButton:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .actions .action .VPButton::before {
|
||||||
|
content: '[';
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .actions .action .VPButton::after {
|
||||||
|
content: ']';
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .image-container .image-bg {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Features === */
|
||||||
|
.VPFeatures .VPFeature {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border: 1px solid var(--vp-c-divider) !important;
|
||||||
|
transition: border-color var(--tui-transition), background var(--tui-transition),
|
||||||
|
transform var(--tui-transition);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFeatures .VPFeature::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
transition: width 280ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFeatures .VPFeature:hover {
|
||||||
|
border-color: var(--vp-c-brand-1) !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFeatures .VPFeature:hover::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFeatures .VPFeature .title {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPFeatures .VPFeature .details {
|
||||||
|
font-family: var(--tui-font-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Outline === */
|
||||||
|
.VPDocAsideOutline .outline-title {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
transition: color var(--tui-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link.active {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocAsideOutline .outline-link.active::before {
|
||||||
|
content: '> ';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Doc footer === */
|
||||||
|
.VPDocFooter .edit-link-button,
|
||||||
|
.VPDocFooter .last-updated-text {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocFooter .pager-link {
|
||||||
|
border-radius: 0;
|
||||||
|
transition: border-color var(--tui-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocFooter .pager-link:hover {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocFooter .pager-link .title {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPDocFooter .pager-link .desc {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Search === */
|
||||||
|
.VPLocalSearchBox .search-input {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Background texture === */
|
||||||
|
.VPDoc,
|
||||||
|
.VPHome {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.015'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 200px 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Hero glow === */
|
||||||
|
.VPHome .VPHero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHome .VPHero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -120px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 600px;
|
||||||
|
height: 400px;
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
var(--tui-hero-glow) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Footer === */
|
||||||
|
.VPFooter {
|
||||||
|
font-family: var(--tui-font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
border-top: 1px solid var(--vp-c-divider);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# SubMiner docs
|
||||||
|
|
||||||
|
In-repo VitePress documentation source for SubMiner.
|
||||||
|
|
||||||
|
Internal architecture/workflow source of truth lives in `docs/README.md` at the repo root. Keep `docs-site/` user-facing.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun --cwd docs-site install
|
||||||
|
bun run docs:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and preview:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run docs:build
|
||||||
|
bun run docs:preview
|
||||||
|
bun run docs:test
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct package commands still work from `docs-site/` if you prefer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docs-site
|
||||||
|
bun install
|
||||||
|
bun run docs:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloudflare Pages
|
||||||
|
|
||||||
|
- Git repo: `ksyasuda/SubMiner`
|
||||||
|
- Production branch: `main`
|
||||||
|
- Automatic production and preview deployments: disabled
|
||||||
|
- Custom domain: `docs.subminer.moe` attached to Production
|
||||||
|
- Deployment path: GitHub Actions direct upload with Wrangler
|
||||||
|
|
||||||
|
The public docs root is stable-only:
|
||||||
|
|
||||||
|
- `/` serves the latest stable release docs.
|
||||||
|
- `/main/` serves development docs from `main`.
|
||||||
|
- `/v/<version>/` serves stable release archives.
|
||||||
|
- `/versions` (root build only) lists every published version.
|
||||||
|
- Prerelease tags do not update the docs site.
|
||||||
|
|
||||||
|
Only `/` is indexable. `/main/` and every `/v/<version>/` page carries a self-referential canonical plus `noindex,follow`, repeated as an `X-Robots-Tag` header (by the generated `_headers` file for `/main/`, by the archive function for `/v/`). They stay crawlable so their links still resolve, but ~30 archived copies of every page would otherwise consume the crawl budget the current docs need. Only the root build emits `sitemap.xml`, and its `<lastmod>` dates come from `git log` against the tracked checkout at the released tag, because the build renders from an untracked snapshot that VitePress cannot date itself.
|
||||||
|
|
||||||
|
Keep Cloudflare Git auto-deploy disabled. The production deploy is `.github/workflows/docs-pages.yml`, which uploads `.tmp/docs-versioned-site` with `--branch main` so tag-triggered runs update Production instead of creating preview deployments.
|
||||||
|
|
||||||
|
### Stable archives in R2
|
||||||
|
|
||||||
|
`/v/<version>/` archives are not part of the Pages deployment. Each one is built once, uploaded to an R2 bucket under `v/<version>/`, and served by the Pages Function in `functions/v/[[path]].ts`. Each deploy builds only archives the bucket is missing (an archive counts as present once its `_archive.json` marker exists), plus the root and `/main/` builds. Archive nav links to `/versions` instead of listing releases, so a new tag never invalidates old archives.
|
||||||
|
|
||||||
|
To re-render archives on purpose (theme change, docs fix), run the `Docs Pages` workflow manually with `rebuild_archives` set to comma-separated tags or `all`.
|
||||||
|
|
||||||
|
One-time setup:
|
||||||
|
|
||||||
|
- R2 bucket for archives; its name goes in the `DOCS_ARCHIVE_R2_BUCKET` repository variable.
|
||||||
|
- R2 API token with Object Read & Write on that bucket; its S3 credentials go in the `DOCS_ARCHIVE_R2_ACCESS_KEY_ID` and `DOCS_ARCHIVE_R2_SECRET_ACCESS_KEY` repository secrets.
|
||||||
|
- Pages project: Settings > Bindings > R2 bucket, variable name `DOCS_ARCHIVES`, pointing at the same bucket.
|
||||||
|
|
||||||
|
The first deploy after setup builds and uploads every stable archive; later deploys only add new tags.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# AniList integration
|
||||||
|
|
||||||
|
SubMiner updates your [AniList](https://anilist.co) watch progress when you finish an episode. The same connection supplies cover art for the stats dashboard and names for the [character dictionary](/character-dictionary).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Set `anilist.enabled` to `true`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"anilist": {
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Restart SubMiner. With no token stored, it opens the AniList setup window. You can also open it from the tray (**Configure AniList**) or with `subminer app --anilist-setup`.
|
||||||
|
3. Approve access on the AniList page. SubMiner receives the token through a `subminer://` link and stores it encrypted.
|
||||||
|
|
||||||
|
If the setup window does not render, SubMiner opens the authorization page in your browser instead. To skip the flow entirely, paste a token into `anilist.accessToken`.
|
||||||
|
|
||||||
|
On Linux, the token is stored with `gnome-libsecret` by default. If your keyring is unavailable, start it (gnome-keyring or KWallet) or launch SubMiner with `--password-store=basic_text`.
|
||||||
|
|
||||||
|
## How updates work
|
||||||
|
|
||||||
|
An episode counts as watched after 85% of its length and at least 10 minutes of playback. SubMiner then:
|
||||||
|
|
||||||
|
1. Reads the title, season, and episode from the file name and folder. Install [guessit](https://github.com/guessit-io/guessit) for better parsing. A folder named `Season 2` is a strong season hint.
|
||||||
|
2. Finds the matching AniList entry. For season 2 and later, it follows the show's sequels.
|
||||||
|
3. Sets your progress to that episode and marks the entry Watching, or Completed on the final episode.
|
||||||
|
|
||||||
|
The show must already be on your Planning or Watching list. SubMiner does not add new entries, and it never lowers your progress.
|
||||||
|
|
||||||
|
Failed updates are saved and retried in the background, up to 8 times with growing delays. The queue survives restarts.
|
||||||
|
|
||||||
|
## Fixing a wrong match
|
||||||
|
|
||||||
|
If a cover or title in the stats Library is wrong, open the title and use **Change AniList Entry**.
|
||||||
|
|
||||||
|
If SubMiner cannot find a later season, it skips the update rather than writing progress to season 1. Pin the right entry with the character dictionary's AniList override. See [Character dictionary](/character-dictionary).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| ------------------------------------ | --------------------------------------- |
|
||||||
|
| `subminer app --anilist-setup` | Open the AniList setup window |
|
||||||
|
| `subminer app --anilist-status` | Show token state and retry queue counts |
|
||||||
|
| `subminer app --anilist-logout` | Remove the stored token |
|
||||||
|
| `subminer app --anilist-retry-queue` | Retry one queued update now |
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
| Key | What it does |
|
||||||
|
| --------------------- | ------------------------------------------------------------- |
|
||||||
|
| `anilist.enabled` | Turns on progress updates. |
|
||||||
|
| `anilist.accessToken` | Token override. Leave empty to use the token stored by setup. |
|
||||||
|
|
||||||
|
Character dictionary settings live under `anilist.characterDictionary` and are covered on the [Character dictionary](/character-dictionary) page. See [Configuration](/configuration#anilist) for defaults.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**No update after an episode.** Check that `anilist.enabled` is `true` and that you watched at least 85% of the episode.
|
||||||
|
|
||||||
|
**"AniList update not possible."** Add the show to your Planning or Watching list, then mark the episode watched again.
|
||||||
|
|
||||||
|
**Wrong show or episode.** Install guessit and make sure it is on your `PATH`. Unusual file names parse poorly without it.
|
||||||
|
|
||||||
|
**Token errors.** Run `subminer app --anilist-status`. If the token is invalid, run `--anilist-logout`, then `--anilist-setup`.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# AniSkip integration
|
||||||
|
|
||||||
|
SubMiner looks up opening timestamps on [AniSkip](https://aniskip.com) so you can skip an anime's intro with one key.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
AniSkip is on by default. To turn it off or change the key:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"mpv": {
|
||||||
|
"aniskipEnabled": true,
|
||||||
|
"aniskipButtonKey": "TAB",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both settings apply immediately, without restarting mpv. For better title and episode detection, install [guessit](https://github.com/guessit-io/guessit):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install --user guessit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
When a local file loads, SubMiner reads the title and episode from the file name, finds the show on MyAnimeList, and asks AniSkip for the intro's timestamps. Streams and URLs are skipped.
|
||||||
|
|
||||||
|
If AniSkip has an intro, SubMiner adds `AniSkip Intro Start` and `AniSkip Intro End` chapters. When the intro starts, mpv shows "You can skip by pressing TAB" (with your key) for 3 seconds. Press the key any time during the intro to jump to its end.
|
||||||
|
|
||||||
|
With a custom key other than `TAB` or `y-k`, `y-k` also skips.
|
||||||
|
|
||||||
|
## Triggering from mpv
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| ----------------------------------------- | ------------------------------------------------------- |
|
||||||
|
| `script-message subminer-skip-intro` | Skip to the end of the intro |
|
||||||
|
| `script-message subminer-aniskip-refresh` | Look up the current file again, ignoring cached results |
|
||||||
|
|
||||||
|
Use `subminer-aniskip-refresh` after a lookup failed or matched the wrong show.
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Anki integration
|
||||||
|
|
||||||
|
SubMiner talks to Anki through the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on. It fills new cards with the sentence, an audio clip, and a screenshot, and can create sentence cards and merge duplicate words. It is built for the [Lapis](https://github.com/donkuri/lapis), [Kiku](https://kiku.youyoumu.my.id/), and [Senren](https://github.com/BrenoAqua/Senren) note types, but works with any note type once you map its fields.
|
||||||
|
|
||||||
|
For the day-to-day flow, see [Mining workflow](/mining-workflow). Every key on this page, with its default, is listed in the [AnkiConnect config reference](/configuration#ankiconnect).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Install [Anki](https://apps.ankiweb.net/).
|
||||||
|
2. Install AnkiConnect (add-on code `2055492159`).
|
||||||
|
3. Install FFmpeg and make sure it is on your `PATH`. SubMiner uses it for audio and images.
|
||||||
|
4. Keep Anki running while you mine.
|
||||||
|
|
||||||
|
If you changed AnkiConnect's port, set `ankiConnect.url` to match.
|
||||||
|
|
||||||
|
## How cards get filled
|
||||||
|
|
||||||
|
When Yomitan adds a note, SubMiner fills the sentence, audio, image, and MiscInfo fields. It finds new notes in one of two ways:
|
||||||
|
|
||||||
|
- **Proxy (default).** SubMiner runs a local AnkiConnect-compatible server. Yomitan sends notes through it, and SubMiner fills each one right after Anki accepts it.
|
||||||
|
- **Polling.** With `ankiConnect.proxy.enabled` set to `false`, SubMiner asks AnkiConnect for recently added notes every `ankiConnect.pollingRate` milliseconds.
|
||||||
|
|
||||||
|
Set `ankiConnect.behavior.autoUpdateNewCards` to `false` to stop automatic filling and update cards by hand with `Ctrl/Cmd+V` instead.
|
||||||
|
|
||||||
|
`ankiConnect.deck` limits enrichment and duplicate checks to one deck. If it is empty, SubMiner uses Yomitan's mining deck when it can read it, and otherwise searches all decks.
|
||||||
|
|
||||||
|
### Proxy mode setup (Yomitan / texthooker) {#proxy-mode-setup-yomitan-texthooker}
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"ankiConnect": {
|
||||||
|
"url": "http://127.0.0.1:8765",
|
||||||
|
"proxy": {
|
||||||
|
"enabled": true,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8766,
|
||||||
|
"upstreamUrl": "http://127.0.0.1:8765"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients must send notes to the proxy (`http://127.0.0.1:8766` here), not to AnkiConnect directly.
|
||||||
|
|
||||||
|
- **Bundled Yomitan.** SubMiner sets the active Yomitan profile's Anki server for you. With the proxy on, it always points the profile at the proxy. With the proxy off, it sets `ankiConnect.url`, but only if the profile's server is blank or the stock `http://127.0.0.1:8765`.
|
||||||
|
- **Browser Yomitan or other clients.** Set the Anki server to the proxy URL yourself. To leave your main profile alone, create a separate Yomitan profile for SubMiner, set its Anki server (Settings, Anki) to the proxy URL, and make it active while you mine.
|
||||||
|
|
||||||
|
### Proxy troubleshooting
|
||||||
|
|
||||||
|
If cards are not getting filled:
|
||||||
|
|
||||||
|
1. Check that the proxy is listening while SubMiner runs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ss -ltnp | grep 8766
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Check that requests pass through to Anki:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS http://127.0.0.1:8766 \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-d '{"action":"version","version":2}'
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Read the app log (`app-YYYY-MM-DD.log`) in the logs folder. See [Troubleshooting](/troubleshooting) for where logs live.
|
||||||
|
|
||||||
|
## Field mapping
|
||||||
|
|
||||||
|
`ankiConnect.fields` maps SubMiner's data to fields on your note type.
|
||||||
|
|
||||||
|
| Key | Receives |
|
||||||
|
| ------------------ | ------------------------------------------------------------------------- |
|
||||||
|
| `fields.word` | The mined word |
|
||||||
|
| `fields.audio` | Sentence audio cut from the video |
|
||||||
|
| `fields.wordAudio` | Read only: Yomitan's word audio, used to time animated images (see below) |
|
||||||
|
| `fields.image` | Screenshot or animated clip |
|
||||||
|
| `fields.sentence` | Subtitle text |
|
||||||
|
| `fields.miscInfo` | Text from `ankiConnect.metadata.pattern` |
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"ankiConnect": {
|
||||||
|
"fields": {
|
||||||
|
"audio": "SentenceAudio",
|
||||||
|
"sentence": "Sentence"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Field names are matched case-insensitively. A mapped field that is missing from the note type is skipped.
|
||||||
|
|
||||||
|
`fields.audio` gets sentence audio, not word audio. Yomitan writes its own dictionary audio into your note, so point `fields.audio` at a separate field such as `SentenceAudio`. The default, `ExpressionAudio`, is the field many note types use for Yomitan's word audio, so leaving it would overwrite that audio.
|
||||||
|
|
||||||
|
`ankiConnect.tags` adds tags to every mined or updated card. Set it to `[]` to add none.
|
||||||
|
|
||||||
|
`ankiConnect.metadata.pattern` builds the MiscInfo text. Tokens: `%f` file name, `%F` file name with extension, `%t` timestamp, `%T` timestamp with milliseconds, `<br>` line break.
|
||||||
|
|
||||||
|
## Media
|
||||||
|
|
||||||
|
| Key | What it does |
|
||||||
|
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `media.generateAudio` | Cut sentence audio (MP3) from the subtitle's start and end time |
|
||||||
|
| `media.audioPadding` | Seconds added before and after the clip |
|
||||||
|
| `media.fallbackDuration` | Clip length when the subtitle has no timing |
|
||||||
|
| `media.maxMediaDuration` | Longest allowed clip, in seconds (`0` removes the cap) |
|
||||||
|
| `media.normalizeAudio` | Normalize clip loudness |
|
||||||
|
| `media.mirrorMpvVolume` | Scale the clip by mpv's current volume, so quiet playback gives quiet clips |
|
||||||
|
| `media.generateImage` | Capture an image |
|
||||||
|
| `media.imageType` | `static` for one frame, `avif` for an animated clip of the line |
|
||||||
|
| `media.imageFormat` | Static format: `jpg`, `png`, or `webp` |
|
||||||
|
| `media.imageQuality` | Static image quality |
|
||||||
|
| `media.imageMaxWidth` / `Height` | Static size limit (`0` keeps source size) |
|
||||||
|
| `media.animatedFps` | Animated clip frame rate |
|
||||||
|
| `media.animatedMaxWidth` / `Height` | Animated size limit (`0` keeps aspect ratio) |
|
||||||
|
| `media.animatedCrf` | Animated quality, `0` to `63`, lower is better |
|
||||||
|
| `media.syncAnimatedImageToWordAudio` | Hold the first frame for the length of the word audio in `fields.wordAudio`, so the motion starts with the sentence audio |
|
||||||
|
| `media.reviewTiming` | Pause and let you adjust the clip before media is made (see below) |
|
||||||
|
|
||||||
|
Animated AVIF needs an FFmpeg build with an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`).
|
||||||
|
|
||||||
|
Media settings apply to the next card without a restart.
|
||||||
|
|
||||||
|
### Review media timing
|
||||||
|
|
||||||
|
With `media.reviewTiming` on, SubMiner pauses before making media for word, sentence, and audio cards and opens a review dialog. You can also toggle it for the current session with **Review Media Timing** in the runtime options palette (`Ctrl/Cmd+Shift+O`). Clipboard updates and stats-dashboard mining skip the review.
|
||||||
|
|
||||||
|
The dialog shows the clip over a speech waveform. When the waveform loads, an untouched clip end moves back to just after the last speech in the line. The orange bars still mark the subtitle's original start and end.
|
||||||
|
|
||||||
|
During preview, the yellow cursor follows the audio player's position through the selected range and stays at the end when playback finishes. Buffering pauses the cursor too. If the player stops reporting progress for 15 seconds, the preview stops so you can retry.
|
||||||
|
|
||||||
|
| Action | How |
|
||||||
|
| ------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| Trim | Drag either edge, or click the waveform to move the nearer edge there |
|
||||||
|
| Nudge an edge | Arrow keys on a focused edge (100 ms, `Shift` for 500 ms) |
|
||||||
|
| Slide the clip | Drag the middle |
|
||||||
|
| Show more timeline | Earlier / Later |
|
||||||
|
| Pick the screenshot | Screenshot slider or Frame buttons (static images only) |
|
||||||
|
| Add previous / next line | `P` / `N` (`Shift+P` / `Shift+N` removes) |
|
||||||
|
| Preview | `Space` |
|
||||||
|
| Confirm | `Enter` |
|
||||||
|
| Cancel | `Escape` |
|
||||||
|
|
||||||
|
The confirmed range is used as is, with no extra padding. Added lines go into the sentence field. Reset restores the original timing and removes added lines.
|
||||||
|
|
||||||
|
When you cancel, you can go back to editing, keep the original timing, create the card without media, or discard it. Discard deletes the Yomitan note or audio card, and skips creation for a sentence card.
|
||||||
|
|
||||||
|
### Update behavior
|
||||||
|
|
||||||
|
| Key | What it does |
|
||||||
|
| ----------------------------- | ---------------------------------------------------- |
|
||||||
|
| `behavior.overwriteAudio` | Replace existing audio instead of adding to it |
|
||||||
|
| `behavior.overwriteImage` | Replace the existing image instead of adding to it |
|
||||||
|
| `behavior.mediaInsertMode` | `append` or `prepend` new media when not overwriting |
|
||||||
|
| `behavior.autoUpdateNewCards` | Fill new Yomitan notes automatically |
|
||||||
|
| `behavior.highlightWord` | Bold the mined word in the sentence field |
|
||||||
|
| `behavior.notificationType` | `overlay`, `system`, `both`, or `none` |
|
||||||
|
|
||||||
|
Manual clipboard updates (`Ctrl/Cmd+V`) always replace the sentence audio, whatever `overwriteAudio` says.
|
||||||
|
|
||||||
|
## Sentence cards (Lapis) {#sentence-cards-lapis}
|
||||||
|
|
||||||
|
`Ctrl/Cmd+S` creates a standalone sentence card from the current line, and `Ctrl/Cmd+Shift+S` then a digit combines several lines. The card uses the note type named in `ankiConnect.isLapis.sentenceCardModel`, which must exist in Anki. If it is empty, no card is created.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"ankiConnect": {
|
||||||
|
"isLapis": {
|
||||||
|
"enabled": true,
|
||||||
|
"sentenceCardModel": "Lapis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sentence cards and audio cards (`Ctrl/Cmd+Shift+A`) always write to the `Sentence` and `SentenceAudio` fields. Normal word cards keep using your `ankiConnect.fields` mapping.
|
||||||
|
|
||||||
|
## Word card type (Kiku/Lapis)
|
||||||
|
|
||||||
|
When `isKiku` or `isLapis` is enabled, SubMiner sets a card-type flag on word cards it fills. Choose the flag with `ankiConnect.lapisKiku.wordCardKind`:
|
||||||
|
|
||||||
|
| Value | Flag |
|
||||||
|
| ------------------- | ----------------------- |
|
||||||
|
| `word-and-sentence` | `IsWordAndSentenceCard` |
|
||||||
|
| `click` | `IsClickCard` |
|
||||||
|
| `sentence` | `IsSentenceCard` |
|
||||||
|
| `audio` | `IsAudioCard` |
|
||||||
|
| `none` | Leaves flags alone |
|
||||||
|
|
||||||
|
The other card-type flags are cleared. Sentence cards and audio cards keep their own flag.
|
||||||
|
|
||||||
|
## Field grouping (Kiku/Senren) {#field-grouping-kiku-senren}
|
||||||
|
|
||||||
|
When you mine a word that already has a card, SubMiner can merge the new card into the old one. The sentence, audio, image, and MiscInfo from both cards are kept as grouped entries, and the template lets you switch between them. This works with [Kiku](https://github.com/youyoumu/kiku) and [Senren](https://github.com/BrenoAqua/Senren) (which calls it [scene switching](https://github.com/BrenoAqua/Senren/blob/main/docs/scene_switching.md)).
|
||||||
|
|
||||||
|
Enable one of them. They write different markup to the same fields, so only one can be on. If both are enabled, Kiku is used and SubMiner logs a config warning.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"ankiConnect": {
|
||||||
|
"isKiku": {
|
||||||
|
"enabled": true,
|
||||||
|
"fieldGrouping": "manual",
|
||||||
|
"deleteDuplicateInAuto": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For Senren, use the same keys under `isSenren`.
|
||||||
|
|
||||||
|
| `fieldGrouping` | Behavior |
|
||||||
|
| --------------- | ------------------------------------------------------------------------------- |
|
||||||
|
| `disabled` | No duplicate check |
|
||||||
|
| `auto` | Merge into the existing card. With `deleteDuplicateInAuto`, delete the new card |
|
||||||
|
| `manual` | Show both cards, let you choose which to keep and preview the merge |
|
||||||
|
|
||||||
|
The manual dialog cancels itself after 90 seconds. Identical entries are not deduplicated. Press `Ctrl/Cmd+G` to run the duplicate check on the last card yourself.
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
| ----------- | ------------------------------------- |
|
||||||
|
| `1` / `2` | Keep card 1 or card 2 |
|
||||||
|
| `Enter` | Confirm |
|
||||||
|
| `Backspace` | Back from the merge preview |
|
||||||
|
| `Esc` | Cancel and leave both cards unchanged |
|
||||||
|
|
||||||
|
## Config validation
|
||||||
|
|
||||||
|
Invalid `ankiConnect` values produce a warning and fall back to the default. Use JSON booleans (`true`, not `"true"`) and a positive number for `pollingRate`.
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
A contributor-facing map of how SubMiner is put together. The canonical internal guidance, including domain ownership and layering rules, is [`docs/architecture/README.md`](https://github.com/ksyasuda/SubMiner/blob/main/docs/architecture/README.md) in the repo.
|
||||||
|
|
||||||
|
SubMiner runs as three cooperating runtimes:
|
||||||
|
|
||||||
|
- the Electron desktop app (`src/`): overlay, UI, and runtime orchestration
|
||||||
|
- the launcher CLI (`launcher/`): mpv and app command workflows
|
||||||
|
- the mpv Lua plugin (`plugin/subminer/`): player-side controls and handoff to the app
|
||||||
|
|
||||||
|
Inside the app, `src/main.ts` is a composition root. It owns wiring and state, and delegates behavior to small runtime and domain modules that can be tested without Electron or mpv.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
launcher/
|
||||||
|
main.ts # entrypoint and command dispatch
|
||||||
|
commands/ # one module per subcommand (playback, jellyfin, stats, sync, ...)
|
||||||
|
config/ # launcher config readers and CLI parser
|
||||||
|
plugin/subminer/ # mpv plugin; main.lua loads init.lua, which boots the other modules
|
||||||
|
src/
|
||||||
|
main-entry.ts # bootstrap wrapper that runs before main.js
|
||||||
|
main.ts # composition root
|
||||||
|
preload*.ts # preload bridges (overlay, settings, stats, sync, Jellyfin setup)
|
||||||
|
main/ # main-process runtime modules and IPC/CLI wiring
|
||||||
|
boot/ # pre-ready boot helpers
|
||||||
|
runtime/composers/ # larger runtime clusters assembled for main.ts
|
||||||
|
runtime/domains/ # domain barrels (startup, overlay, mpv, ipc, shortcuts, anilist, jellyfin, mining)
|
||||||
|
core/services/ # focused services: mpv client, overlay, tokenizer, mining, integrations, stats
|
||||||
|
core/utils/ # pure helpers
|
||||||
|
shared/ipc/ # IPC channel constants and payload validators
|
||||||
|
renderer/ # overlay renderer: subtitle rendering, input handlers, modals
|
||||||
|
config/ # definitions/ (defaults + option registries) and resolve/ (resolution pipeline)
|
||||||
|
cli/ # app CLI parsing and help output
|
||||||
|
settings/, syncui/ # settings and sync windows
|
||||||
|
window-trackers/ # Hyprland, Sway, X11, macOS, and Windows trackers
|
||||||
|
anki-integration/ # AnkiConnect proxy and note-update workflow
|
||||||
|
jimaku/, subsync/, tsukihime/ # integration helpers
|
||||||
|
types/ # shared domain types
|
||||||
|
stats/ # stats dashboard UI (Vite)
|
||||||
|
vendor/ # Yomitan fork, texthooker-ui, JLPT vocab
|
||||||
|
```
|
||||||
|
|
||||||
|
A few ownership notes that are hard to guess from file names:
|
||||||
|
|
||||||
|
- mpv access is split into transport (`mpv-transport.ts`), protocol (`mpv-protocol.ts`), and property modules under `src/core/services/`.
|
||||||
|
- The renderer keeps `renderer.ts` to orchestration. Keyboard, mouse, and gamepad input live in `renderer/handlers/`, and each modal flow has its own file in `renderer/modals/`.
|
||||||
|
- AniSkip intro detection runs in the app (`src/main/runtime/aniskip-runtime.ts`), which drives mpv chapters and the skip key over the mpv IPC socket. The plugin does not handle it.
|
||||||
|
|
||||||
|
## Component diagram
|
||||||
|
|
||||||
|
The main process drives one primary overlay window plus modal surfaces. Primary and secondary subtitle layers render in the same overlay renderer, connected to the main process through `preload.ts`. The launcher and mpv plugin run as separate processes and talk to the app through sockets or CLI passthrough.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
|
||||||
|
classDef comp fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef rend fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef ext fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
|
||||||
|
subgraph ExtRt["External Runtimes"]
|
||||||
|
direction LR
|
||||||
|
Launcher["Launcher CLI"]:::extrt
|
||||||
|
Plugin["mpv Plugin"]:::extrt
|
||||||
|
end
|
||||||
|
|
||||||
|
Main["main.ts"]:::entry
|
||||||
|
|
||||||
|
subgraph Comp["Composition"]
|
||||||
|
direction LR
|
||||||
|
Startup["Startup & Lifecycle"]:::comp
|
||||||
|
Wiring["Runtime Wiring"]:::comp
|
||||||
|
Composers["Domain Composers"]:::comp
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Svc["Services"]
|
||||||
|
direction LR
|
||||||
|
Mpv["MPV Stack"]:::svc
|
||||||
|
OverlaySvc["Overlay Manager"]:::svc
|
||||||
|
Mining["Mining & Subtitles"]:::svc
|
||||||
|
AnkiProxy["Anki Proxy"]:::svc
|
||||||
|
Integrations["Integrations"]:::svc
|
||||||
|
Tracking["Tracking"]:::svc
|
||||||
|
Config["Config & Options"]:::svc
|
||||||
|
end
|
||||||
|
|
||||||
|
Bridge(["preload.ts"]):::bridge
|
||||||
|
|
||||||
|
subgraph Rend["Renderer"]
|
||||||
|
direction LR
|
||||||
|
OverlayWin["Overlay Window"]:::rend
|
||||||
|
UI["Subtitles & Modals"]:::rend
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Ext["External Systems"]
|
||||||
|
direction LR
|
||||||
|
mpvExt["mpv"]:::ext
|
||||||
|
AnkiExt["AnkiConnect"]:::ext
|
||||||
|
JimakuExt["Jimaku"]:::ext
|
||||||
|
TrackerExt["Window Tracker"]:::ext
|
||||||
|
AnilistExt["AniList"]:::ext
|
||||||
|
JellyfinExt["Jellyfin"]:::ext
|
||||||
|
DiscordExt["Discord"]:::ext
|
||||||
|
end
|
||||||
|
|
||||||
|
Launcher -->|"CLI"| Main
|
||||||
|
Plugin -->|"IPC"| mpvExt
|
||||||
|
|
||||||
|
Main --> Comp
|
||||||
|
Comp --> Svc
|
||||||
|
|
||||||
|
Svc --> Bridge
|
||||||
|
Bridge --> Rend
|
||||||
|
|
||||||
|
mpvExt <-->|"socket"| Mpv
|
||||||
|
AnkiExt <-->|"HTTP"| AnkiProxy
|
||||||
|
JimakuExt <-->|"HTTP"| Integrations
|
||||||
|
TrackerExt <-->|"platform"| OverlaySvc
|
||||||
|
AnilistExt <-->|"HTTP"| Tracking
|
||||||
|
JellyfinExt <-->|"HTTP"| Tracking
|
||||||
|
DiscordExt <-->|"RPC"| Integrations
|
||||||
|
|
||||||
|
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composition pattern
|
||||||
|
|
||||||
|
Runtime code uses dependency injection:
|
||||||
|
|
||||||
|
1. Put the logic in a service under `src/core/services/`, as pure or side-effect-bounded functions.
|
||||||
|
2. Build its runtime dependencies in a `src/main/` module. Pass simple dependencies inline; extract an adapter only when it adds behavior or gets reused.
|
||||||
|
3. Call the service from lifecycle or command wiring.
|
||||||
|
|
||||||
|
`main.ts` gets domain handlers through `createMainRuntimeRegistry()` (`src/main/runtime/registry.ts`), which exposes the barrels in `src/main/runtime/domains/`. Larger clusters, such as app-ready startup, mpv, Jellyfin, AniList tracking, shortcuts, and IPC, are assembled by composers in `src/main/runtime/composers/`. Many handlers take a `*MainDeps` object built by a `createBuild*MainDepsHandler` builder, which keeps side effects out of the unit under test.
|
||||||
|
|
||||||
|
Composers declare their inputs with `ComposerInputs<T>` and results with `ComposerOutputs<T>` from `src/main/runtime/composers/contracts.ts`. A missing dependency then fails at compile time.
|
||||||
|
|
||||||
|
### IPC boundary
|
||||||
|
|
||||||
|
Channel names live in `src/shared/ipc/contracts.ts` and payload validators in `src/shared/ipc/validators.ts`. Renderer payloads are validated at the IPC entry points (`src/core/services/ipc.ts`, `src/core/services/anki-jimaku-ipc.ts`) before any domain handler runs. See [IPC + runtime contracts](/ipc-contracts) for the full rules.
|
||||||
|
|
||||||
|
### Runtime state ownership
|
||||||
|
|
||||||
|
Some domains, such as AniList token, queue, and media-guess state, use reducer-style transitions:
|
||||||
|
|
||||||
|
- Composition modules own the mutable state and expose narrow `get*`/`set*` accessors.
|
||||||
|
- Handlers change another domain's state only through its transition helpers in `src/main/state.ts`, never by mutating the object directly.
|
||||||
|
- A transition may update derived counters or snapshots, but must leave metadata it does not own untouched.
|
||||||
|
- Tests for these domains check both the fields that should change and the ones that should not.
|
||||||
|
|
||||||
|
## Playback startup flow
|
||||||
|
|
||||||
|
A SubMiner-managed launch (the `subminer` launcher, the app's own playback, or the packaged Windows shortcut) starts mpv, injects the plugin, and brings up the overlay. The launcher reads `config.jsonc`, spawns mpv with the IPC socket and the bundled plugin, and passes runtime settings as `--script-opts`. The plugin never reads a config file: the shipped `subminer.conf` has no settings, so command-line options always win.
|
||||||
|
|
||||||
|
Once mpv is up, exactly one of two triggers starts the overlay:
|
||||||
|
|
||||||
|
- On a first launch, the launcher sets `auto_start=yes` and the plugin's `file-loaded` hook starts the app once the socket is ready.
|
||||||
|
- When the app is already running, or for `--start-overlay` and YouTube flows, the launcher attaches over the app control socket and suppresses the plugin's auto-start.
|
||||||
|
|
||||||
|
Both paths end in the same app bring-up, which then runs the program lifecycle below.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
|
||||||
|
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef proc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef app fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef overlay fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
|
||||||
|
Entry["Managed launch<br/>subminer CLI · app · Windows shortcut"]:::entry
|
||||||
|
Entry --> Cfg["Launcher reads config.jsonc<br/>→ plugin runtime config"]:::extrt
|
||||||
|
Cfg --> Spawn["Spawn mpv<br/>--input-ipc-server=/tmp/subminer-socket<br/>--script=plugin/subminer/main.lua<br/>--script-opts=subminer-… (auto_start, backend, …)"]:::proc
|
||||||
|
Spawn --> Boot["Plugin boot · read_options('subminer')<br/>empty subminer.conf; CLI opts win"]:::extrt
|
||||||
|
Boot --> Sock["mpv IPC socket ready"]:::proc
|
||||||
|
Sock --> Who{"Overlay trigger"}:::decision
|
||||||
|
|
||||||
|
Who -->|"app already running, or<br/>--start-overlay / YouTube"| Attach["Launcher startOverlay()<br/>attach via control socket<br/>plugin auto-start suppressed"]:::proc
|
||||||
|
Who -->|"first launch, auto_start=yes"| Self["Plugin file-loaded hook<br/>polls socket → process.start_overlay()"]:::extrt
|
||||||
|
|
||||||
|
Attach --> AppUp
|
||||||
|
Self --> AppUp
|
||||||
|
|
||||||
|
AppUp["Spawn / attach SubMiner app<br/>--start --managed-playback --socket … --backend …"]:::app
|
||||||
|
AppUp --> Ctrl["App control server up<br/>/tmp/subminer-control-* dedupes a 2nd launch"]:::app
|
||||||
|
Ctrl --> Life["app.whenReady → Program Lifecycle (below)"]:::app
|
||||||
|
Life --> Conn["MpvIpcClient connects to /tmp/subminer-socket"]:::overlay
|
||||||
|
Conn --> Show["Transparent overlay over mpv<br/>Yomitan lookup · mine"]:::overlay
|
||||||
|
```
|
||||||
|
|
||||||
|
The sockets in this flow are described in [IPC + runtime contracts](./ipc-contracts#runtime-sockets).
|
||||||
|
|
||||||
|
## Program lifecycle
|
||||||
|
|
||||||
|
1. **Module init.** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs services, and wires dependencies. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
|
||||||
|
2. **Startup.** `--generate-config` writes the template and exits. Otherwise `app-lifecycle.ts` takes the single-instance lock and registers Electron lifecycle hooks.
|
||||||
|
3. **App ready.** `composeAppReadyRuntime()` reloads config strictly, resolves keybindings, creates the `MpvIpcClient` (which connects and observes subtitle and playback properties), and starts the runtime options manager, subtitle timing tracker, and immersion tracker.
|
||||||
|
4. **Overlay.** `initializeOverlayRuntime()` creates the overlay window, registers global shortcuts, and tracks mpv's window bounds through the active window tracker. `src/main/runtime/overlay-mpv-sub-visibility.ts` hides mpv's own subtitles while the overlay shows them.
|
||||||
|
5. **Background warmups.** MeCab, Yomitan, JLPT and frequency dictionaries, the optional Jellyfin remote session, Discord presence, AniList token refresh, and the optional AnkiConnect proxy start asynchronously. `startupWarmups` controls which run; its low-power mode defers everything except Yomitan.
|
||||||
|
6. **Runtime.** Event-driven. mpv property changes, IPC messages, CLI commands, shortcuts, and config hot-reloads route through handlers and composers. Subtitle text goes through `SubtitleProcessingController` (normalize, tokenize, merge) and out to the overlay renderer and modals.
|
||||||
|
7. **Shutdown.** `onWillQuitCleanup` tears down the tray, config watcher, shortcuts, WebSocket and texthooker servers, mpv socket, window tracker, and Yomitan parser window. It flushes the immersion tracker to SQLite and stops Jellyfin, Discord, and the AnkiConnect proxy.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
classDef start fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
|
||||||
|
classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef shutdown fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
|
||||||
|
CLI["CLI + Environment"]:::start
|
||||||
|
CLI --> Init["Module Init"]:::phase
|
||||||
|
Init --> Parse["Parse argv"]:::phase
|
||||||
|
Parse --> GenCheck{"--generate-config?"}:::decision
|
||||||
|
GenCheck -->|"yes"| GenExit["Write & exit"]:::phase
|
||||||
|
GenCheck -->|"no"| Lock["Acquire lock"]:::phase
|
||||||
|
|
||||||
|
Lock -->|"app.whenReady()"| Ready["App Ready"]:::phase
|
||||||
|
|
||||||
|
Ready --> Config["Config + keybindings"]:::init
|
||||||
|
Ready --> MpvInit["MPV socket connect"]:::init
|
||||||
|
Ready --> Platform["Runtime services"]:::init
|
||||||
|
|
||||||
|
Config & MpvInit & Platform --> OverlayInit["Overlay Init"]:::phase
|
||||||
|
|
||||||
|
OverlayInit --> MainWin["Create window"]:::init
|
||||||
|
OverlayInit --> Shortcuts["Register shortcuts"]:::init
|
||||||
|
|
||||||
|
MainWin & Shortcuts --> Warmups
|
||||||
|
|
||||||
|
subgraph Warmups["Background Warmups (parallel)"]
|
||||||
|
direction LR
|
||||||
|
W1["MeCab"]:::warmup ~~~ W2["Yomitan"]:::warmup ~~~ W3["Dictionaries"]:::warmup ~~~ W4["Jellyfin"]:::warmup ~~~ W5["Discord"]:::warmup ~~~ W6["AniList"]:::warmup ~~~ W7["Anki Proxy"]:::warmup
|
||||||
|
end
|
||||||
|
|
||||||
|
Warmups --> Loop
|
||||||
|
|
||||||
|
subgraph Loop["Event Loop"]
|
||||||
|
direction TB
|
||||||
|
Events["mpv · IPC · shortcuts · config"]:::runtime
|
||||||
|
Events --> Route["Composers"]:::runtime
|
||||||
|
Route --> Pipeline["Subtitle Pipeline"]:::runtime
|
||||||
|
Pipeline --> Broadcast["State + Renderer"]:::runtime
|
||||||
|
end
|
||||||
|
|
||||||
|
style Warmups fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
|
||||||
|
Loop -->|"quit"| Quit["Shutdown"]:::shutdown
|
||||||
|
|
||||||
|
subgraph Cleanup[" "]
|
||||||
|
direction LR
|
||||||
|
T1["UI cleanup"]:::shutdown
|
||||||
|
T2["Socket + server teardown"]:::shutdown
|
||||||
|
T3["Flush tracking + state"]:::shutdown
|
||||||
|
end
|
||||||
|
|
||||||
|
Quit --> Cleanup
|
||||||
|
|
||||||
|
style Cleanup fill:transparent,stroke:none
|
||||||
|
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subtitle prefetch
|
||||||
|
|
||||||
|
SubMiner tokenizes upcoming subtitle lines before they appear, so they render from a warm cache. `SubtitlePrefetchService` (`src/core/services/subtitle-prefetch.ts`) gets the cue list from the active track: an external subtitle file, or for local media an embedded text track extracted with ffmpeg. It parses the cues with `subtitle-cue-parser.ts`, picks a window of upcoming lines from the playback position, and tokenizes them through the live pipeline, storing results in the `SubtitleProcessingController` cache.
|
||||||
|
|
||||||
|
Live subtitle processing always takes priority; the prefetcher pauses while the on-screen line is being processed. It recomputes its window on seek and re-prefetches when the cache is invalidated, for example after a word is marked known.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
||||||
|
|
||||||
|
SubFile["Subtitle Track"]:::init
|
||||||
|
Parse["Cue Parser"]:::phase
|
||||||
|
Window["Upcoming Lines"]:::phase
|
||||||
|
Tokenize["Pre-tokenize"]:::warmup
|
||||||
|
Cache["Token Cache"]:::runtime
|
||||||
|
Appear["Subtitle Appears"]:::init
|
||||||
|
Hit["Cache Hit"]:::runtime
|
||||||
|
Render["Fast Render"]:::runtime
|
||||||
|
|
||||||
|
SubFile --> Parse --> Window --> Tokenize --> Cache
|
||||||
|
Appear --> Hit --> Render
|
||||||
|
Cache -.->|"warm"| Hit
|
||||||
|
|
||||||
|
style SubFile stroke-width:2px
|
||||||
|
style Render stroke-width:2px
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extension rules
|
||||||
|
|
||||||
|
- Add behavior to a service in `src/core/services/` or a focused module under `src/main/runtime/`. Keep new logic out of `main.ts`.
|
||||||
|
- For changes that span startup, overlay, mpv, or integration wiring, compose through `src/main/runtime/domains/` and `src/main/runtime/composers/` instead of wiring directly in `main.ts`.
|
||||||
|
- Add a cross-process channel in `src/shared/ipc/contracts.ts` first, validate it in `src/shared/ipc/validators.ts`, then wire the handler. See [IPC + runtime contracts](/ipc-contracts#add-a-new-ipc-action).
|
||||||
|
- Config is split by domain under `src/config/definitions/` and `src/config/resolve/`. Keep config changes in the matching domain file.
|
||||||
|
- Keep CLI flags and IPC channels stable unless a change is meant to break them.
|
||||||
|
- Add or update focused tests when a runtime boundary or contract changes, including malformed-payload tests for IPC.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { expect, test } from 'bun:test';
|
||||||
|
import { onRequest, resolveArchiveRoute, type ArchiveBucket } from './functions/v/[[path]]';
|
||||||
|
|
||||||
|
// In-memory stand-in for the R2 binding, including the range and precondition behavior
|
||||||
|
// the function relies on.
|
||||||
|
function fakeBucket(objects: Record<string, string>): ArchiveBucket {
|
||||||
|
return {
|
||||||
|
async get(key, options) {
|
||||||
|
const content = objects[key];
|
||||||
|
if (content === undefined) return null;
|
||||||
|
const bytes = new TextEncoder().encode(content);
|
||||||
|
const httpEtag = `"${key}"`;
|
||||||
|
|
||||||
|
if (options?.onlyIf?.get('if-none-match') === httpEtag) {
|
||||||
|
return { size: bytes.length, httpEtag };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeHeader = options?.range?.get('range');
|
||||||
|
const match = rangeHeader ? /^bytes=(\d+)-(\d*)$/.exec(rangeHeader) : null;
|
||||||
|
if (match) {
|
||||||
|
const offset = Number(match[1]);
|
||||||
|
const end = match[2] ? Number(match[2]) : bytes.length - 1;
|
||||||
|
const slice = bytes.slice(offset, end + 1);
|
||||||
|
return {
|
||||||
|
size: bytes.length,
|
||||||
|
httpEtag,
|
||||||
|
range: { offset, length: slice.length },
|
||||||
|
body: new Blob([slice]).stream(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { size: bytes.length, httpEtag, body: new Blob([bytes]).stream() };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = fakeBucket({
|
||||||
|
'v/0.19.6/index.html': '<h1>home</h1>',
|
||||||
|
'v/0.19.6/usage.html': '<h1>usage</h1>',
|
||||||
|
'v/0.19.6/404.html': '<h1>missing</h1>',
|
||||||
|
'v/0.19.6/assets/app.abc123.js': 'console.log(1)',
|
||||||
|
});
|
||||||
|
|
||||||
|
function request(path: string, init?: RequestInit) {
|
||||||
|
return onRequest({
|
||||||
|
request: new Request(`https://docs.subminer.moe${path}`, init),
|
||||||
|
env: { DOCS_ARCHIVES: bucket },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('archive routes follow the clean-URL layout of the built archives', () => {
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6/usage')).toEqual({
|
||||||
|
kind: 'lookup',
|
||||||
|
version: '0.19.6',
|
||||||
|
keys: ['v/0.19.6/usage.html', 'v/0.19.6/usage/index.html'],
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6/')).toEqual({
|
||||||
|
kind: 'lookup',
|
||||||
|
version: '0.19.6',
|
||||||
|
keys: ['v/0.19.6/index.html'],
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6', '?q=1')).toEqual({
|
||||||
|
kind: 'redirect',
|
||||||
|
location: '/v/0.19.6/?q=1',
|
||||||
|
status: 301,
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/')).toEqual({
|
||||||
|
kind: 'redirect',
|
||||||
|
location: '/versions',
|
||||||
|
status: 302,
|
||||||
|
});
|
||||||
|
expect(resolveArchiveRoute('/v/0.19.6/%2e%2e/secret')).toEqual({ kind: 'not-found' });
|
||||||
|
expect(resolveArchiveRoute('/v/latest/')).toEqual({ kind: 'not-found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serves archive pages and hashed assets with their cache policy', async () => {
|
||||||
|
const page = await request('/v/0.19.6/usage');
|
||||||
|
expect(page.status).toBe(200);
|
||||||
|
expect(page.headers.get('content-type')).toBe('text/html; charset=utf-8');
|
||||||
|
expect(page.headers.get('cache-control')).toBe('public, max-age=3600');
|
||||||
|
expect(page.headers.get('x-robots-tag')).toBe('noindex, follow');
|
||||||
|
expect(await page.text()).toBe('<h1>usage</h1>');
|
||||||
|
|
||||||
|
const asset = await request('/v/0.19.6/assets/app.abc123.js');
|
||||||
|
expect(asset.headers.get('content-type')).toBe('text/javascript; charset=utf-8');
|
||||||
|
expect(asset.headers.get('cache-control')).toContain('immutable');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing archive pages fall back to the archive 404 page', async () => {
|
||||||
|
const response = await request('/v/0.19.6/nope');
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
expect(await response.text()).toBe('<h1>missing</h1>');
|
||||||
|
|
||||||
|
const unknownVersion = await request('/v/0.1.0/usage');
|
||||||
|
expect(unknownVersion.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports byte ranges, conditional requests, and HEAD', async () => {
|
||||||
|
const partial = await request('/v/0.19.6/usage', { headers: { Range: 'bytes=4-8' } });
|
||||||
|
expect(partial.status).toBe(206);
|
||||||
|
expect(partial.headers.get('content-range')).toBe('bytes 4-8/14');
|
||||||
|
expect(await partial.text()).toBe('usage');
|
||||||
|
|
||||||
|
const notModified = await request('/v/0.19.6/usage', {
|
||||||
|
headers: { 'If-None-Match': '"v/0.19.6/usage.html"' },
|
||||||
|
});
|
||||||
|
expect(notModified.status).toBe(304);
|
||||||
|
|
||||||
|
const head = await request('/v/0.19.6/usage', { method: 'HEAD' });
|
||||||
|
expect(head.status).toBe(200);
|
||||||
|
expect(head.headers.get('content-length')).toBe('14');
|
||||||
|
expect(await head.text()).toBe('');
|
||||||
|
|
||||||
|
const post = await request('/v/0.19.6/usage', { method: 'POST' });
|
||||||
|
expect(post.status).toBe(405);
|
||||||
|
});
|
||||||
@@ -0,0 +1,623 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "subminer-docs",
|
||||||
|
"dependencies": {
|
||||||
|
"@catppuccin/vitepress": "^0.1.2",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
|
"@fontsource/manrope": "^5.2.8",
|
||||||
|
"mermaid": "^11.12.3",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitepress": "^1.6.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@algolia/abtesting": ["@algolia/abtesting@1.15.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw=="],
|
||||||
|
|
||||||
|
"@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.17.7", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q=="],
|
||||||
|
|
||||||
|
"@algolia/autocomplete-plugin-algolia-insights": ["@algolia/autocomplete-plugin-algolia-insights@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A=="],
|
||||||
|
|
||||||
|
"@algolia/autocomplete-preset-algolia": ["@algolia/autocomplete-preset-algolia@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA=="],
|
||||||
|
|
||||||
|
"@algolia/autocomplete-shared": ["@algolia/autocomplete-shared@1.17.7", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg=="],
|
||||||
|
|
||||||
|
"@algolia/client-abtesting": ["@algolia/client-abtesting@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw=="],
|
||||||
|
|
||||||
|
"@algolia/client-analytics": ["@algolia/client-analytics@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g=="],
|
||||||
|
|
||||||
|
"@algolia/client-common": ["@algolia/client-common@5.49.1", "", {}, "sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg=="],
|
||||||
|
|
||||||
|
"@algolia/client-insights": ["@algolia/client-insights@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g=="],
|
||||||
|
|
||||||
|
"@algolia/client-personalization": ["@algolia/client-personalization@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w=="],
|
||||||
|
|
||||||
|
"@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg=="],
|
||||||
|
|
||||||
|
"@algolia/client-search": ["@algolia/client-search@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA=="],
|
||||||
|
|
||||||
|
"@algolia/ingestion": ["@algolia/ingestion@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ=="],
|
||||||
|
|
||||||
|
"@algolia/monitoring": ["@algolia/monitoring@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw=="],
|
||||||
|
|
||||||
|
"@algolia/recommend": ["@algolia/recommend@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw=="],
|
||||||
|
|
||||||
|
"@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA=="],
|
||||||
|
|
||||||
|
"@algolia/requester-fetch": ["@algolia/requester-fetch@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw=="],
|
||||||
|
|
||||||
|
"@algolia/requester-node-http": ["@algolia/requester-node-http@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA=="],
|
||||||
|
|
||||||
|
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
|
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||||
|
|
||||||
|
"@catppuccin/vitepress": ["@catppuccin/vitepress@0.1.2", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-dqhgo6U6GWbgh3McAgwemUC8Y2Aj48rRcQx/9iuPzBPAgo7NA3yi7ZcR0wolAENMmoOMAHBV+rz/5DfiGxtZLA=="],
|
||||||
|
|
||||||
|
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.2", "", { "dependencies": { "@chevrotain/gast": "11.1.2", "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q=="],
|
||||||
|
|
||||||
|
"@chevrotain/gast": ["@chevrotain/gast@11.1.2", "", { "dependencies": { "@chevrotain/types": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g=="],
|
||||||
|
|
||||||
|
"@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.2", "", {}, "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw=="],
|
||||||
|
|
||||||
|
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
|
||||||
|
|
||||||
|
"@chevrotain/utils": ["@chevrotain/utils@11.1.2", "", {}, "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA=="],
|
||||||
|
|
||||||
|
"@docsearch/css": ["@docsearch/css@3.8.2", "", {}, "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ=="],
|
||||||
|
|
||||||
|
"@docsearch/js": ["@docsearch/js@3.8.2", "", { "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ=="],
|
||||||
|
|
||||||
|
"@docsearch/react": ["@docsearch/react@3.8.2", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", "react": ">= 16.8.0 < 19.0.0", "react-dom": ">= 16.8.0 < 19.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||||
|
|
||||||
|
"@fontsource/jetbrains-mono": ["@fontsource/jetbrains-mono@5.2.8", "", {}, "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ=="],
|
||||||
|
|
||||||
|
"@fontsource/manrope": ["@fontsource/manrope@5.2.8", "", {}, "sha512-gJHJmcuUk7qWcNCfcAri/DJQtXtBYqi9yKratr4jXhSo0I3xUtNNKI+igQIcw5c+m95g0vounk8ZnX/kb8o0TA=="],
|
||||||
|
|
||||||
|
"@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.72", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-wkcixntHvaCoqPqerGrNFcHQ3Yx1ux4ZkhscCDK0DEHpP62XCH+cxq1HTsRjbUiQl/M9K8bj03HF6Wgn5iE2rQ=="],
|
||||||
|
|
||||||
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
|
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
||||||
|
|
||||||
|
"@shikijs/core": ["@shikijs/core@2.5.0", "", { "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg=="],
|
||||||
|
|
||||||
|
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^3.1.0" } }, "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w=="],
|
||||||
|
|
||||||
|
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw=="],
|
||||||
|
|
||||||
|
"@shikijs/langs": ["@shikijs/langs@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w=="],
|
||||||
|
|
||||||
|
"@shikijs/themes": ["@shikijs/themes@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw=="],
|
||||||
|
|
||||||
|
"@shikijs/transformers": ["@shikijs/transformers@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/types": "2.5.0" } }, "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg=="],
|
||||||
|
|
||||||
|
"@shikijs/types": ["@shikijs/types@2.5.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw=="],
|
||||||
|
|
||||||
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
|
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||||
|
|
||||||
|
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||||
|
|
||||||
|
"@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
|
||||||
|
|
||||||
|
"@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
|
||||||
|
|
||||||
|
"@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
|
||||||
|
|
||||||
|
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||||
|
|
||||||
|
"@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
|
||||||
|
|
||||||
|
"@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||||
|
|
||||||
|
"@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
|
||||||
|
|
||||||
|
"@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
|
||||||
|
|
||||||
|
"@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
|
||||||
|
|
||||||
|
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||||
|
|
||||||
|
"@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
|
||||||
|
|
||||||
|
"@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
|
||||||
|
|
||||||
|
"@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
|
||||||
|
|
||||||
|
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
|
||||||
|
|
||||||
|
"@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
|
||||||
|
|
||||||
|
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||||
|
|
||||||
|
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||||
|
|
||||||
|
"@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
|
||||||
|
|
||||||
|
"@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
|
||||||
|
|
||||||
|
"@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
|
||||||
|
|
||||||
|
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||||
|
|
||||||
|
"@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||||
|
|
||||||
|
"@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
|
||||||
|
|
||||||
|
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
|
||||||
|
|
||||||
|
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||||
|
|
||||||
|
"@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
|
||||||
|
|
||||||
|
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||||
|
|
||||||
|
"@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
|
||||||
|
|
||||||
|
"@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||||
|
|
||||||
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
||||||
|
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
|
||||||
|
|
||||||
|
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
|
||||||
|
|
||||||
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
|
|
||||||
|
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
|
||||||
|
|
||||||
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
|
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||||
|
|
||||||
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
|
||||||
|
|
||||||
|
"@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
|
||||||
|
|
||||||
|
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
|
||||||
|
|
||||||
|
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
|
||||||
|
|
||||||
|
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
|
||||||
|
|
||||||
|
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
||||||
|
|
||||||
|
"@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
|
||||||
|
|
||||||
|
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
||||||
|
|
||||||
|
"@vue/reactivity": ["@vue/reactivity@3.5.29", "", { "dependencies": { "@vue/shared": "3.5.29" } }, "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA=="],
|
||||||
|
|
||||||
|
"@vue/runtime-core": ["@vue/runtime-core@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg=="],
|
||||||
|
|
||||||
|
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/runtime-core": "3.5.29", "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg=="],
|
||||||
|
|
||||||
|
"@vue/server-renderer": ["@vue/server-renderer@3.5.29", "", { "dependencies": { "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "vue": "3.5.29" } }, "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g=="],
|
||||||
|
|
||||||
|
"@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||||
|
|
||||||
|
"@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="],
|
||||||
|
|
||||||
|
"@vueuse/integrations": ["@vueuse/integrations@12.8.2", "", { "dependencies": { "@vueuse/core": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" }, "peerDependencies": { "async-validator": "^4", "axios": "^1", "change-case": "^5", "drauu": "^0.4", "focus-trap": "^7", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", "nprogress": "^0.2", "qrcode": "^1.5", "sortablejs": "^1", "universal-cookie": "^7" }, "optionalPeers": ["async-validator", "axios", "change-case", "drauu", "focus-trap", "fuse.js", "idb-keyval", "jwt-decode", "nprogress", "qrcode", "sortablejs", "universal-cookie"] }, "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g=="],
|
||||||
|
|
||||||
|
"@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
|
||||||
|
|
||||||
|
"@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"algoliasearch": ["algoliasearch@5.49.1", "", { "dependencies": { "@algolia/abtesting": "1.15.1", "@algolia/client-abtesting": "5.49.1", "@algolia/client-analytics": "5.49.1", "@algolia/client-common": "5.49.1", "@algolia/client-insights": "5.49.1", "@algolia/client-personalization": "5.49.1", "@algolia/client-query-suggestions": "5.49.1", "@algolia/client-search": "5.49.1", "@algolia/ingestion": "1.49.1", "@algolia/monitoring": "1.49.1", "@algolia/recommend": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ=="],
|
||||||
|
|
||||||
|
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||||
|
|
||||||
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
|
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||||
|
|
||||||
|
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||||
|
|
||||||
|
"chevrotain": ["chevrotain@11.1.2", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", "@chevrotain/regexp-to-ast": "11.1.2", "@chevrotain/types": "11.1.2", "@chevrotain/utils": "11.1.2", "lodash-es": "4.17.23" } }, "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg=="],
|
||||||
|
|
||||||
|
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
|
||||||
|
|
||||||
|
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||||
|
|
||||||
|
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
|
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
||||||
|
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
||||||
|
|
||||||
|
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="],
|
||||||
|
|
||||||
|
"cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
|
||||||
|
|
||||||
|
"cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
|
||||||
|
|
||||||
|
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
|
||||||
|
|
||||||
|
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||||
|
|
||||||
|
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
|
||||||
|
|
||||||
|
"d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
|
||||||
|
|
||||||
|
"d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
|
||||||
|
|
||||||
|
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||||
|
|
||||||
|
"d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
|
||||||
|
|
||||||
|
"d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||||
|
|
||||||
|
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||||
|
|
||||||
|
"d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
|
||||||
|
|
||||||
|
"d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||||
|
|
||||||
|
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||||
|
|
||||||
|
"d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
|
||||||
|
|
||||||
|
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||||
|
|
||||||
|
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||||
|
|
||||||
|
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||||
|
|
||||||
|
"d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||||
|
|
||||||
|
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||||
|
|
||||||
|
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||||
|
|
||||||
|
"d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||||
|
|
||||||
|
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||||
|
|
||||||
|
"d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||||
|
|
||||||
|
"d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
|
||||||
|
|
||||||
|
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||||
|
|
||||||
|
"d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||||
|
|
||||||
|
"d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
|
||||||
|
|
||||||
|
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||||
|
|
||||||
|
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||||
|
|
||||||
|
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||||
|
|
||||||
|
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||||
|
|
||||||
|
"d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
|
||||||
|
|
||||||
|
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
|
||||||
|
|
||||||
|
"dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="],
|
||||||
|
|
||||||
|
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
||||||
|
|
||||||
|
"delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
|
"dompurify": ["dompurify@3.3.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ=="],
|
||||||
|
|
||||||
|
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
|
||||||
|
|
||||||
|
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||||
|
|
||||||
|
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||||
|
|
||||||
|
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||||
|
|
||||||
|
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
|
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||||
|
|
||||||
|
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
||||||
|
|
||||||
|
"katex": ["katex@0.16.35", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-S0+riEvy1CK4VKse1ivMff8gmabe/prY7sKB3njjhyoLLsNFDQYtKNgXrbWUggGDCJBz7Fctl5i8fLCESHXzSg=="],
|
||||||
|
|
||||||
|
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
||||||
|
|
||||||
|
"langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="],
|
||||||
|
|
||||||
|
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
|
||||||
|
|
||||||
|
"lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="],
|
||||||
|
|
||||||
|
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||||
|
|
||||||
|
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||||
|
|
||||||
|
"mermaid": ["mermaid@11.12.3", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ=="],
|
||||||
|
|
||||||
|
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||||
|
|
||||||
|
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||||
|
|
||||||
|
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||||
|
|
||||||
|
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||||
|
|
||||||
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
|
"minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="],
|
||||||
|
|
||||||
|
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||||
|
|
||||||
|
"mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"oniguruma-to-es": ["oniguruma-to-es@3.1.1", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ=="],
|
||||||
|
|
||||||
|
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||||
|
|
||||||
|
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
|
"points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
||||||
|
|
||||||
|
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
|
|
||||||
|
"preact": ["preact@10.28.4", "", {}, "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ=="],
|
||||||
|
|
||||||
|
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||||
|
|
||||||
|
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||||
|
|
||||||
|
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||||
|
|
||||||
|
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||||
|
|
||||||
|
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||||
|
|
||||||
|
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||||
|
|
||||||
|
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
||||||
|
|
||||||
|
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="],
|
||||||
|
|
||||||
|
"shiki": ["shiki@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||||
|
|
||||||
|
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
||||||
|
|
||||||
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
|
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
||||||
|
|
||||||
|
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
||||||
|
|
||||||
|
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
|
|
||||||
|
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||||
|
|
||||||
|
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||||
|
|
||||||
|
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||||
|
|
||||||
|
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||||
|
|
||||||
|
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||||
|
|
||||||
|
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||||
|
|
||||||
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
||||||
|
|
||||||
|
"vitepress": ["vitepress@1.6.4", "", { "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", "@shikijs/core": "^2.1.0", "@shikijs/transformers": "^2.1.0", "@shikijs/types": "^2.1.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", "@vue/devtools-api": "^7.7.0", "@vue/shared": "^3.5.13", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "focus-trap": "^7.6.4", "mark.js": "8.11.1", "minisearch": "^7.1.1", "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg=="],
|
||||||
|
|
||||||
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||||
|
|
||||||
|
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
||||||
|
|
||||||
|
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||||
|
|
||||||
|
"vue": ["vue@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", "@vue/runtime-dom": "3.5.29", "@vue/server-renderer": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA=="],
|
||||||
|
|
||||||
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
||||||
|
|
||||||
|
"d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||||
|
|
||||||
|
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||||
|
|
||||||
|
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||||
|
|
||||||
|
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||||
|
|
||||||
|
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Character dictionary
|
||||||
|
|
||||||
|
SubMiner builds a Yomitan dictionary of the characters in the show you are watching, using data from [AniList](https://anilist.co). Character names in subtitles get their own color, and hovering one shows the character's portrait, role, voice actor, and description.
|
||||||
|
|
||||||
|
Ordinary dictionaries rarely contain character names, so without this every name counts as an unknown word and throws off [N+1 highlighting](/subtitle-annotations#n-1-word-highlighting).
|
||||||
|
|
||||||
|
## Turning it on
|
||||||
|
|
||||||
|
1. Set `subtitleStyle.nameMatchEnabled` to `true`, or turn it on in the Settings window under Annotation Display, Character Names.
|
||||||
|
2. Optionally set `subtitleStyle.nameMatchImagesEnabled` to `true` to show a small portrait next to each name in the subtitle line.
|
||||||
|
3. Play an episode.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"nameMatchEnabled": true,
|
||||||
|
"nameMatchImagesEnabled": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
No AniList account is needed. Logging in to AniList is only for [watch progress sync](/anilist-integration).
|
||||||
|
|
||||||
|
The character dictionary does not work when `yomitan.externalProfilePath` is set, because SubMiner then uses another app's Yomitan profile read-only.
|
||||||
|
|
||||||
|
## What happens when you play something
|
||||||
|
|
||||||
|
When a new show starts, SubMiner:
|
||||||
|
|
||||||
|
1. Guesses the title from the filename and finds it on AniList.
|
||||||
|
2. Downloads the cast list and portraits.
|
||||||
|
3. Builds the dictionary and imports it into SubMiner's Yomitan.
|
||||||
|
|
||||||
|
A notification shows each step. Once it says the dictionary is ready, names match from the next subtitle line.
|
||||||
|
|
||||||
|
Each character gets entries for the full name, family name, given name, and common honorifics (`さん`, `君`, `ちゃん`, `先生`, and others), so `太郎さん` matches as well as `太郎`.
|
||||||
|
|
||||||
|
SubMiner keeps your most recent shows loaded in one merged dictionary. `anilist.characterDictionary.maxLoaded` sets how many. Starting another show drops the oldest one. Only the current show's characters are highlighted.
|
||||||
|
|
||||||
|
### How long it takes
|
||||||
|
|
||||||
|
The first time you watch a show, most of the time goes into downloading portraits, one at a time. A typical cast takes seconds to a minute. A very large cast takes much longer: One Piece has over a thousand characters and takes around 10 minutes. After that the show is cached, and later episodes load it from disk.
|
||||||
|
|
||||||
|
## Correcting AniList matches
|
||||||
|
|
||||||
|
SubMiner can match the wrong show when a filename is ambiguous, for example `Re - ZERO, Starting Life in Another World (2016)` matching a different `Re...` series. To fix it:
|
||||||
|
|
||||||
|
1. Press `Ctrl/Cmd+D` to open the character dictionary manager.
|
||||||
|
2. Click **Override**, edit the title if needed, search, and pick the right result.
|
||||||
|
|
||||||
|
From the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List AniList matches for a file
|
||||||
|
subminer dictionary --candidates "/path/to/episode.mkv"
|
||||||
|
|
||||||
|
# Save the correct AniList ID for that series
|
||||||
|
subminer dictionary --select 21355 "/path/to/episode.mkv"
|
||||||
|
```
|
||||||
|
|
||||||
|
The override applies to every episode of that season in the same folder. Other seasons are not affected, even when they share the folder. The override also sets which entry [AniList watch progress](/anilist-integration) updates, so one fix covers both.
|
||||||
|
|
||||||
|
## Managing loaded shows
|
||||||
|
|
||||||
|
The manager (`Ctrl/Cmd+D`) lists the shows in the merged dictionary and marks the current one.
|
||||||
|
|
||||||
|
- **Remove** drops a show from the dictionary. You cannot remove the show you are watching.
|
||||||
|
- **Up/Down** changes which show gets dropped first when a new one is added.
|
||||||
|
- **Override** replaces a show's AniList match.
|
||||||
|
|
||||||
|
## Generating from the command line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
subminer dictionary /path/to/media
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds a standalone dictionary file for that file or folder without playing it. With the AppImage directly, use `SubMiner.AppImage --dictionary`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Defaults are in the [configuration reference](/configuration).
|
||||||
|
|
||||||
|
| Key | What it does |
|
||||||
|
| ---------------------------------------------------------------------- | ------------------------------------------------ |
|
||||||
|
| `subtitleStyle.nameMatchEnabled` | Build the dictionary and color character names |
|
||||||
|
| `subtitleStyle.nameMatchImagesEnabled` | Show a portrait next to matched names |
|
||||||
|
| `subtitleStyle.nameMatchColor` | Color for character names |
|
||||||
|
| `anilist.characterDictionary.maxLoaded` | Number of recent shows kept in the dictionary |
|
||||||
|
| `anilist.characterDictionary.collapsibleSections.description` | Show the description expanded in the popup |
|
||||||
|
| `anilist.characterDictionary.collapsibleSections.characterInformation` | Show age, birthday, and similar details expanded |
|
||||||
|
| `anilist.characterDictionary.collapsibleSections.voicedBy` | Show the voice actor section expanded |
|
||||||
|
| `shortcuts.openCharacterDictionaryManager` | Shortcut for the manager |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**It seems stuck.** Check the notification. While generating it shows counts (`image 120/400`), an estimate of time left, and an elapsed clock. If the clock moves, it is still working, and large casts are slow (see [how long it takes](#how-long-it-takes)). If you missed the notification, open the notification history with `Ctrl/Cmd+N`. For errors, check the app log ([log locations](/troubleshooting)).
|
||||||
|
|
||||||
|
**Import failed or timed out.** Yomitan may have been busy importing another dictionary. Play the next episode or restart SubMiner. The import may have finished anyway, so check for the character popup before retrying.
|
||||||
|
|
||||||
|
**Names are not highlighted.** Check that `subtitleStyle.nameMatchEnabled` is `true`, that `yomitan.externalProfilePath` is empty, and that the show was found on AniList. The wrong show's cast means a wrong match. See [correcting AniList matches](#correcting-anilist-matches).
|
||||||
|
|
||||||
|
**Portraits are missing.** Portraits need AniList to have an image and the download to succeed. If you were offline during the first sync, delete that show's file from `character-dictionaries/snapshots/` in the SubMiner config directory and replay it.
|
||||||
|
|
||||||
|
SubMiner's generator is based on the [Japanese Character Name Dictionary](https://github.com/bee-san/Japanese_Character_Name_Dictionary) project, which also supports VNDB and works without SubMiner.
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
---
|
||||||
|
outline: [2, 3]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { withBase } from 'vitepress';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
All SubMiner settings live in one file, `config.jsonc`. Most of them are also editable in the Settings window, so you rarely need to edit the file by hand. This page lists every config block with its keys and defaults.
|
||||||
|
|
||||||
|
## Config file {#configuration-file}
|
||||||
|
|
||||||
|
| Platform | Path |
|
||||||
|
| ------------ | ------------------------------------------------------------------------------------- |
|
||||||
|
| Linux, macOS | `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (`~/.config/SubMiner/config.jsonc` if unset) |
|
||||||
|
| Windows | `%APPDATA%\SubMiner\config.jsonc` |
|
||||||
|
|
||||||
|
The file is JSONC, so comments and trailing commas are allowed. If both `config.jsonc` and `config.json` exist, SubMiner uses `config.jsonc`. Only add the keys you want to change. Everything else uses the built-in default.
|
||||||
|
|
||||||
|
The [generated example config](/config.example.jsonc) lists every option with its default and a comment. Defaults in the tables below come from that file.
|
||||||
|
|
||||||
|
::: warning mpv.socketPath differs on Windows
|
||||||
|
The example shows `"socketPath": "/tmp/subminer-socket"`. On Windows the default is `\\.\pipe\subminer-socket`. Leave `mpv.socketPath` out of your config unless you need a custom path, and SubMiner picks the right one.
|
||||||
|
:::
|
||||||
|
|
||||||
|
To write a fresh default config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SubMiner.AppImage --generate-config
|
||||||
|
SubMiner.AppImage --generate-config --config-path /tmp/subminer.jsonc
|
||||||
|
SubMiner.AppImage --generate-config --backup-overwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
If the target file exists, SubMiner asks before backing it up and overwriting it. In non-interactive shells, pass `--backup-overwrite`.
|
||||||
|
|
||||||
|
A syntax error in the file stops startup with a message that names the file. A valid file with a bad value logs a warning and uses the default for that key. On macOS, these warnings also open a dialog.
|
||||||
|
|
||||||
|
## Settings window {#settings}
|
||||||
|
|
||||||
|
Open it from the tray menu, with `subminer settings`, or with the app's `--settings` flag. Options are grouped by task (Appearance, Behavior, Mining & Anki, Input, Integrations, Tracking & App, Advanced) rather than by config block, but each field saves to its normal `config.jsonc` path.
|
||||||
|
|
||||||
|
- Saving keeps your comments, trailing commas, and unrelated keys. Resetting a field removes its key so the default applies.
|
||||||
|
- Each field is tagged **Live** or **Restart**. After saving, a banner lists any sections that need a restart.
|
||||||
|
- Anki fields can fetch deck, note type, and field names from AnkiConnect.
|
||||||
|
- Secret fields never show the stored value, only whether one is set. Prefer the `*Command` variants (such as `jimaku.apiKeyCommand`) to keep keys out of the file.
|
||||||
|
|
||||||
|
## Hot-reload {#hot-reload-behavior}
|
||||||
|
|
||||||
|
SubMiner watches the config file while running. When it changes, live settings apply immediately and SubMiner shows a notification listing any changed sections that need a restart. If the new file is invalid, the previous config stays active.
|
||||||
|
|
||||||
|
These apply live:
|
||||||
|
|
||||||
|
- `subtitleStyle`, `subtitleSidebar`, `subtitleSelection`, `keybindings`, `shortcuts`
|
||||||
|
- `logging.level`, `logging.rotation`, `logging.files`
|
||||||
|
- `secondarySub.defaultMode`, `youtube.primarySubLanguages`, `youtube.subtitleSource`
|
||||||
|
- `mpv.aniskipEnabled`, `mpv.aniskipButtonKey`, `stats.toggleKey`, `stats.markWatchedKey`
|
||||||
|
- `ankiConnect.deck`, `ankiConnect.fields.*`, `ankiConnect.behavior.autoUpdateNewCards`
|
||||||
|
- `ankiConnect.media.normalizeAudio`, `media.mirrorMpvVolume`, `media.reviewTiming`
|
||||||
|
- `ankiConnect.knownWords` (`highlightEnabled`, `refreshMinutes`, `addMinedWordsImmediately`, `matchMode`, `decks`) and `ankiConnect.nPlusOne.*`
|
||||||
|
- `ankiConnect.isLapis.sentenceCardModel`, `isKiku.fieldGrouping`, `isSenren.fieldGrouping`, `lapisKiku.wordCardKind`
|
||||||
|
|
||||||
|
These are read at the start of the next operation, so changes take effect on the next request or run: `jimaku`, `tmdb`, `subsync`, `subtitleGeneration`, `notifications`.
|
||||||
|
|
||||||
|
Everything else needs a restart.
|
||||||
|
|
||||||
|
## Core settings
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Log files are named by date (`app-YYYY-MM-DD.log`, `launcher-...`, `mpv-...`). Log export writes a sanitized copy and leaves the originals alone.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------ | -------- | ------------------------------------------------------- |
|
||||||
|
| `logging.level` | `"warn"` | Minimum level: `debug`, `info`, `warn`, `error` |
|
||||||
|
| `logging.rotation` | `7` | Days of logs to keep |
|
||||||
|
| `logging.files.app` | `true` | Write app logs |
|
||||||
|
| `logging.files.launcher` | `true` | Write launcher logs |
|
||||||
|
| `logging.files.mpv` | `false` | Write mpv logs. Turn on temporarily to debug mpv/plugin |
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
Manual checks from the tray or `subminer -u` always work, even with automatic checks off. Overlay update notifications include an **Update** button.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ---------------------------- | ----------- | --------------------------------------------------------- |
|
||||||
|
| `updates.enabled` | `true` | Check for updates in the background |
|
||||||
|
| `updates.checkIntervalHours` | `24` | Minimum hours between automatic checks |
|
||||||
|
| `updates.notificationType` | `"overlay"` | `overlay`, `system`, `both` (overlay + system), or `none` |
|
||||||
|
| `updates.channel` | `"stable"` | `stable` or `prerelease` (betas and release candidates) |
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
Overlay notifications are also kept in a session-only history panel. Toggle it with `shortcuts.toggleNotificationHistory`. The panel opens from the same side as the notification cards.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------- | ------------- | ---------------------------------------------------------- |
|
||||||
|
| `notifications.overlayPosition` | `"top-right"` | Where overlay cards appear: `top-left`, `top`, `top-right` |
|
||||||
|
|
||||||
|
Mining and startup status notifications use `ankiConnect.behavior.notificationType` (see [AnkiConnect](#ankiconnect)).
|
||||||
|
|
||||||
|
### Auto-start overlay
|
||||||
|
|
||||||
|
When mpv is started by SubMiner or the `subminer` launcher, the launcher passes these settings to the bundled mpv plugin. There is no separate plugin config file. `mpv.autoStartSubMiner` and `mpv.pauseUntilOverlayReady` (see [MPV launcher](#mpv-launcher)) control the background start and the initial pause.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| -------------------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `auto_start_overlay` | `true` | Show the visible overlay when the mpv plugin starts SubMiner |
|
||||||
|
|
||||||
|
### Startup warmups
|
||||||
|
|
||||||
|
Warmups load components in the background at startup. Turn one off to load it on first use instead.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| -------------------------------------- | ------- | ----------------------------------------------------------------------------- |
|
||||||
|
| `startupWarmups.lowPowerMode` | `false` | Defer every warmup except the Yomitan extension |
|
||||||
|
| `startupWarmups.mecab` | `true` | Load the MeCab tokenizer |
|
||||||
|
| `startupWarmups.yomitanExtension` | `true` | Load the Yomitan extension |
|
||||||
|
| `startupWarmups.subtitleDictionaries` | `true` | Load the JLPT and frequency dictionaries |
|
||||||
|
| `startupWarmups.jellyfinRemoteSession` | `false` | Connect the Jellyfin remote session (also needs Jellyfin remote auto-connect) |
|
||||||
|
|
||||||
|
### WebSocket server
|
||||||
|
|
||||||
|
Broadcasts plain subtitle text to external clients. See [WebSocket / Texthooker API](/websocket-texthooker-api) for payloads and client examples.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `websocket.enabled` | `false` | `true`, `false`, or `"auto"` (start unless the [mpv_websocket](https://github.com/kuroahna/mpv_websocket) plugin is installed) |
|
||||||
|
| `websocket.port` | `6677` | Server port |
|
||||||
|
|
||||||
|
### Annotation WebSocket
|
||||||
|
|
||||||
|
A separate stream that adds token data (known word, N+1, frequency, JLPT, character names) to each subtitle. The bundled texthooker uses it.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ----------------------------- | ------- | ------------------------------------------------------- |
|
||||||
|
| `annotationWebsocket.enabled` | `false` | Start the annotated stream (independent of `websocket`) |
|
||||||
|
| `annotationWebsocket.port` | `6678` | Server port |
|
||||||
|
|
||||||
|
### Texthooker
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ---------------------------- | ------- | ------------------------------------------------------- |
|
||||||
|
| `texthooker.launchAtStartup` | `false` | Start the texthooker server when SubMiner starts |
|
||||||
|
| `texthooker.openBrowser` | `false` | Open the texthooker page in your browser when it starts |
|
||||||
|
|
||||||
|
## Subtitle display
|
||||||
|
|
||||||
|
### Subtitle style
|
||||||
|
|
||||||
|
Controls how primary and secondary subtitles look and which annotations they show. `css` and `secondary.css` take CSS declarations with normal property names. See [Subtitle annotations](/subtitle-annotations) for how known-word, N+1, frequency, JLPT, and character-name highlighting work.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"subtitleStyle": {
|
||||||
|
"css": { "font-size": "40px", "color": "#ffffff" },
|
||||||
|
"secondary": { "css": { "font-size": "24px" } },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------------------------ | ------------ | ----------------------------------------------------------------------------------------------------- |
|
||||||
|
| `subtitleStyle.primaryDefaultMode` | `"visible"` | Primary bar at startup: `hidden`, `visible`, or `hover` |
|
||||||
|
| `subtitleStyle.css` | see example | CSS for primary subtitles (font, size `35px`, color, shadow, and so on) |
|
||||||
|
| `subtitleStyle.secondary.css` | see example | CSS for secondary subtitles (size `24px`) |
|
||||||
|
| `subtitleStyle.preserveLineBreaks` | `false` | Keep line breaks as mpv shows them instead of one line |
|
||||||
|
| `subtitleStyle.autoPauseVideoOnHover` | `true` | Pause while the mouse is over subtitle text |
|
||||||
|
| `subtitleStyle.autoPauseVideoOnYomitanPopup` | `true` | Pause while a Yomitan popup is open |
|
||||||
|
| `subtitleStyle.primaryVisibleOnYomitanPopup` | `true` | In hover mode, keep the primary bar visible while a popup is open |
|
||||||
|
| `subtitleStyle.knownWordColor` | `#a6da95` | Known-word highlight color |
|
||||||
|
| `subtitleStyle.knownWordMaturityColors` | see example | `new`, `learning`, `young`, `mature` colors, used when `ankiConnect.knownWords.maturityEnabled` is on |
|
||||||
|
| `subtitleStyle.nPlusOneColor` | `#c6a0f6` | N+1 target word color |
|
||||||
|
| `subtitleStyle.enableJlpt` | `false` | Underline words by JLPT level |
|
||||||
|
| `subtitleStyle.jlptColors` | see example | Underline colors for `N1` to `N5` |
|
||||||
|
| `subtitleStyle.nameMatchEnabled` | `false` | Sync the character dictionary and color character names |
|
||||||
|
| `subtitleStyle.nameMatchImagesEnabled` | `false` | Show small character portraits next to matched names |
|
||||||
|
| `subtitleStyle.nameMatchColor` | `#f5bde6` | Character-name color |
|
||||||
|
| `subtitleStyle.frequencyDictionary.enabled` | `false` | Color words by frequency rank |
|
||||||
|
| `subtitleStyle.frequencyDictionary.sourcePath` | `""` | Folder with `term_meta_bank_*.json` files. Empty searches the default locations |
|
||||||
|
| `subtitleStyle.frequencyDictionary.topX` | `10000` | Only color words ranked at or below this |
|
||||||
|
| `subtitleStyle.frequencyDictionary.mode` | `"single"` | `single` (one color) or `banded` (five colors, common to rare) |
|
||||||
|
| `subtitleStyle.frequencyDictionary.matchMode` | `"headword"` | Look up by `headword` (dictionary form) or `surface` (text as shown) |
|
||||||
|
| `subtitleStyle.frequencyDictionary.singleColor` | `#f5a97f` | Color for `single` mode |
|
||||||
|
| `subtitleStyle.frequencyDictionary.bandedColors` | see example | Five colors for `banded` mode |
|
||||||
|
|
||||||
|
Two CSS custom properties style the hovered word: `--subtitle-hover-token-color` (`#f4dbd6`) and `--subtitle-hover-token-background-color` (`transparent`). Set them inside `subtitleStyle.css`.
|
||||||
|
|
||||||
|
### Subtitle sidebar
|
||||||
|
|
||||||
|
A scrollable cue list for the current subtitle file. It only works when SubMiner could parse the active subtitle into cues. See [Subtitle sidebar](/subtitle-sidebar).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ----------------------------------- | ------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| `subtitleSidebar.enabled` | `true` | Enable the sidebar |
|
||||||
|
| `subtitleSidebar.autoOpen` | `false` | Open it once when the overlay starts |
|
||||||
|
| `subtitleSidebar.layout` | `"overlay"` | `overlay` floats over mpv. `embedded` reserves space on the right of the video |
|
||||||
|
| `subtitleSidebar.toggleKey` | `"Backslash"` | `KeyboardEvent.code` that opens and closes it |
|
||||||
|
| `subtitleSidebar.pauseVideoOnHover` | `true` | Pause while hovering the cue list |
|
||||||
|
| `subtitleSidebar.autoScroll` | `true` | Keep the active cue in view |
|
||||||
|
| `subtitleSidebar.css` | see example | CSS for the sidebar, plus the custom properties below |
|
||||||
|
|
||||||
|
Sidebar custom properties: `--subtitle-sidebar-max-width` (`420px`), `--subtitle-sidebar-timestamp-color`, `--subtitle-sidebar-active-line-color`, `--subtitle-sidebar-active-background-color`, `--subtitle-sidebar-hover-background-color`. Their defaults are in the example config.
|
||||||
|
|
||||||
|
If `embedded` layout places the video oddly on your system, switch back to `overlay`.
|
||||||
|
|
||||||
|
### Subtitle position
|
||||||
|
|
||||||
|
You can also drag subtitles with `Right-click + drag` while watching.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| --------------------------- | ------- | ---------------------------------------------------------------- |
|
||||||
|
| `subtitlePosition.yPercent` | `10` | Starting distance from the bottom, as a percent of screen height |
|
||||||
|
|
||||||
|
### Secondary subtitles
|
||||||
|
|
||||||
|
Shows a second track, such as English, above the Japanese line.
|
||||||
|
|
||||||
|
Secondary subtitles do **not** auto-load by default (`autoLoadSecondarySub`, default: `false`). To load them for local and Jellyfin playback, turn it on and list the languages you want:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"secondarySub": {
|
||||||
|
"secondarySubLanguages": ["eng", "en"],
|
||||||
|
"autoLoadSecondarySub": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------------ | --------- | --------------------------------------------------------------------------------------- |
|
||||||
|
| `secondarySub.secondarySubLanguages` | `[]` | Language codes in priority order. Regular tracks win over Signs/Songs tracks |
|
||||||
|
| `secondarySub.autoLoadSecondarySub` | `false` | Load a matching secondary track when the primary loads |
|
||||||
|
| `secondarySub.defaultMode` | `"hover"` | `hidden`, `visible` (always shown), or `hover` (shown when you hover the subtitle area) |
|
||||||
|
|
||||||
|
YouTube ignores the first two keys and always picks English. See [YouTube integration](/youtube-integration). `defaultMode` applies everywhere.
|
||||||
|
|
||||||
|
### Subtitle selection {#subtitle-selection}
|
||||||
|
|
||||||
|
Adds a modal for choosing mpv's primary and secondary subtitle tracks. Open it with `g` then `s` (`shortcuts.openSubtitleSelection`). While enabled, that shortcut replaces mpv's own binding for the same key. See [Keyboard shortcuts](/shortcuts) for sequence conflicts.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| --------------------------- | ------- | ----------------------------------- |
|
||||||
|
| `subtitleSelection.enabled` | `false` | Enable the subtitle selection modal |
|
||||||
|
|
||||||
|
## Keyboard and controls
|
||||||
|
|
||||||
|
### Keybindings
|
||||||
|
|
||||||
|
`keybindings` maps keys to mpv commands or SubMiner actions. Your entries merge with the defaults. The full default list is on [Keyboard shortcuts](/shortcuts).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keybindings": [
|
||||||
|
{ "key": "Shift+ArrowRight", "command": ["seek", 30] },
|
||||||
|
{ "key": "MBTN_BACK", "command": ["sub-seek", -1] },
|
||||||
|
{ "key": "Space", "command": null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `key` uses `KeyboardEvent.code` names (`Space`, `KeyR`, `ArrowRight`) with optional `Ctrl+`, `Alt+`, `Shift+`, `Meta+`. Mouse buttons are `MBTN_LEFT`, `MBTN_MID`, `MBTN_RIGHT`, `MBTN_BACK`, `MBTN_FORWARD`. Scroll wheel keys are `WHEEL_UP`, `WHEEL_DOWN`, `WHEEL_LEFT`, `WHEEL_RIGHT`.
|
||||||
|
- `command` is any mpv JSON IPC command array. Set it to `null` to disable a default.
|
||||||
|
- Commands starting with `__` run inside SubMiner: `__playlist-browser-open`, `__youtube-picker-open`, `__replay-subtitle`, `__play-next-subtitle`, `__runtime-options-open`, and `__runtime-option-cycle:<id>[:next|prev]`.
|
||||||
|
- Unused single-key, mouse button, and scroll wheel bindings from your mpv config also work in the overlay. Your SubMiner bindings win on conflicts.
|
||||||
|
|
||||||
|
### Shortcuts configuration
|
||||||
|
|
||||||
|
`shortcuts` holds SubMiner's own actions (mining, copying, opening modals). Values are [Electron accelerator strings](https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts) such as `"CommandOrControl+S"`. Set one to `null` to disable it. [Keyboard shortcuts](/shortcuts) lists every key, its default, and what it does. Anki shortcuts only run when `ankiConnect.enabled` is on.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------ | ------- | -------------------------------------------------------- |
|
||||||
|
| `shortcuts.multiCopyTimeoutMs` | `3000` | How long multi-copy and multi-mine wait for a digit (ms) |
|
||||||
|
|
||||||
|
### Controller support
|
||||||
|
|
||||||
|
Gamepad input for the overlay, through the browser Gamepad API. It only works while keyboard-only mode is on. Use the `Alt+C` modal to pick a controller and learn bindings, and `Alt+Shift+C` to see raw button and axis values. Default button actions are on [Keyboard shortcuts](/shortcuts).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ---------------------------------- | -------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| `controller.enabled` | `false` | Enable controller support. The `Alt+C` and `Alt+Shift+C` modals stay closed while off |
|
||||||
|
| `controller.smoothScroll` | `true` | Smooth popup scrolling |
|
||||||
|
| `controller.scrollPixelsPerSecond` | `900` | Popup scroll speed |
|
||||||
|
| `controller.horizontalJumpPixels` | `160` | Popup page-jump distance |
|
||||||
|
| `controller.stickDeadzone` | `0.2` | Stick deadzone |
|
||||||
|
| `controller.triggerInputMode` | `"auto"` | `auto`, `digital`, or `analog`. Use `analog` if your L2/R2 report analog values |
|
||||||
|
| `controller.triggerDeadzone` | `0.5` | Trigger threshold for `auto` and `analog` |
|
||||||
|
| `controller.repeatDelayMs` | `320` | Delay before a held button repeats |
|
||||||
|
| `controller.repeatIntervalMs` | `120` | Repeat interval for held buttons |
|
||||||
|
|
||||||
|
Bindings are set with `Alt+C` learn mode, which saves them per controller.
|
||||||
|
|
||||||
|
## Anki integration
|
||||||
|
|
||||||
|
### AnkiConnect
|
||||||
|
|
||||||
|
Creates and updates Anki cards with sentence, audio, and screenshot. Needs the [AnkiConnect](https://github.com/FooSoft/anki-connect) add-on and ffmpeg. See [Anki integration](/anki-integration) for setup, the proxy, and media options in detail.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ankiConnect": {
|
||||||
|
"deck": "Mining",
|
||||||
|
"fields": { "audio": "SentenceAudio", "image": "Picture" },
|
||||||
|
"knownWords": { "highlightEnabled": true, "decks": { "Mining": ["Expression"] } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Connection**
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------- | ------------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| `ankiConnect.enabled` | `true` | Enable Anki integration |
|
||||||
|
| `ankiConnect.url` | `"http://127.0.0.1:8765"` | AnkiConnect URL |
|
||||||
|
| `ankiConnect.pollingRate` | `3000` | Milliseconds between checks for new cards (polling mode) |
|
||||||
|
| `ankiConnect.proxy.enabled` | `true` | Run a local AnkiConnect proxy so cards added through it are updated right away |
|
||||||
|
| `ankiConnect.proxy.host` | `"127.0.0.1"` | Proxy bind host |
|
||||||
|
| `ankiConnect.proxy.port` | `8766` | Proxy bind port |
|
||||||
|
| `ankiConnect.proxy.upstreamUrl` | `"http://127.0.0.1:8765"` | Where the proxy forwards requests |
|
||||||
|
| `ankiConnect.tags` | `["SubMiner"]` | Tags added to mined and updated cards. `[]` disables |
|
||||||
|
| `ankiConnect.deck` | `""` | Deck for duplicate checks and enrichment. Empty uses Yomitan's mining deck |
|
||||||
|
|
||||||
|
**Fields**
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `ankiConnect.fields.word` | `"Expression"` | Word field |
|
||||||
|
| `ankiConnect.fields.audio` | `"ExpressionAudio"` | Field that receives sentence audio. Set a separate field such as `SentenceAudio` so it does not overwrite Yomitan's word audio |
|
||||||
|
| `ankiConnect.fields.wordAudio` | `"ExpressionAudio"` | Existing word-audio field, read only to time animated images |
|
||||||
|
| `ankiConnect.fields.image` | `"Picture"` | Screenshot field |
|
||||||
|
| `ankiConnect.fields.sentence` | `"Sentence"` | Sentence field |
|
||||||
|
| `ankiConnect.fields.miscInfo` | `"MiscInfo"` | Metadata field. `null` disables |
|
||||||
|
| `ankiConnect.metadata.pattern` | `"[SubMiner] %f (%t)"` | MiscInfo template: `%f` filename, `%F` filename with extension, `%t` time, `%T` time with ms, `<br>` newline |
|
||||||
|
|
||||||
|
**Media**
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------------------------ | ---------- | ------------------------------------------------------------ |
|
||||||
|
| `ankiConnect.media.generateAudio` | `true` | Cut a sentence audio clip |
|
||||||
|
| `ankiConnect.media.generateImage` | `true` | Capture a screenshot or animation |
|
||||||
|
| `ankiConnect.media.imageType` | `"static"` | `static` or `avif` (animated) |
|
||||||
|
| `ankiConnect.media.imageFormat` | `"jpg"` | Static format: `jpg`, `png`, `webp` |
|
||||||
|
| `ankiConnect.media.imageQuality` | `92` | JPG/WebP quality. PNG ignores it |
|
||||||
|
| `ankiConnect.media.imageMaxWidth` | `0` | Max static width in px. `0` keeps the source size |
|
||||||
|
| `ankiConnect.media.imageMaxHeight` | `0` | Max static height in px. `0` keeps the source size |
|
||||||
|
| `ankiConnect.media.animatedFps` | `10` | AVIF frame rate |
|
||||||
|
| `ankiConnect.media.animatedMaxWidth` | `640` | AVIF max width |
|
||||||
|
| `ankiConnect.media.animatedMaxHeight` | `0` | AVIF max height. `0` keeps the aspect ratio |
|
||||||
|
| `ankiConnect.media.animatedCrf` | `35` | AVIF quality. Lower is better and larger |
|
||||||
|
| `ankiConnect.media.syncAnimatedImageToWordAudio` | `true` | Hold the first AVIF frame for the length of the word audio |
|
||||||
|
| `ankiConnect.media.normalizeAudio` | `true` | Normalize clip loudness |
|
||||||
|
| `ankiConnect.media.mirrorMpvVolume` | `true` | Apply mpv's current volume to the clip |
|
||||||
|
| `ankiConnect.media.reviewTiming` | `false` | Pause and let you adjust clip timing before media is created |
|
||||||
|
| `ankiConnect.media.audioPadding` | `0` | Seconds added to both ends of audio and AVIF clips |
|
||||||
|
| `ankiConnect.media.fallbackDuration` | `3` | Clip length in seconds when subtitle timing is missing |
|
||||||
|
| `ankiConnect.media.maxMediaDuration` | `30` | Longest allowed clip in seconds. `0` removes the cap |
|
||||||
|
|
||||||
|
**Behavior**
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ----------------------------------------- | ----------- | ------------------------------------------------------------------------ |
|
||||||
|
| `ankiConnect.behavior.autoUpdateNewCards` | `true` | Fill new cards automatically. When off, use the manual shortcuts |
|
||||||
|
| `ankiConnect.behavior.overwriteAudio` | `true` | Replace existing audio. When off, add alongside it |
|
||||||
|
| `ankiConnect.behavior.overwriteImage` | `true` | Replace existing images. When off, add alongside them |
|
||||||
|
| `ankiConnect.behavior.mediaInsertMode` | `"append"` | `append` or `prepend` when not overwriting |
|
||||||
|
| `ankiConnect.behavior.highlightWord` | `true` | Bold the mined word in the sentence field |
|
||||||
|
| `ankiConnect.behavior.notificationType` | `"overlay"` | Where mining and status messages go: `overlay`, `system`, `both`, `none` |
|
||||||
|
|
||||||
|
**Known words and N+1**
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------------------------- | ------------ | -------------------------------------------------------------------------------- |
|
||||||
|
| `ankiConnect.knownWords.highlightEnabled` | `false` | Highlight words that already exist in your Anki decks |
|
||||||
|
| `ankiConnect.knownWords.decks` | `{}` | Decks and word fields to read, for example `{ "Kaishi 1.5k": ["Word"] }` |
|
||||||
|
| `ankiConnect.knownWords.matchMode` | `"headword"` | Match by `headword` or `surface` text |
|
||||||
|
| `ankiConnect.knownWords.refreshMinutes` | `1440` | Minutes between cache refreshes |
|
||||||
|
| `ankiConnect.knownWords.addMinedWordsImmediately` | `true` | Add newly mined words to the cache right away |
|
||||||
|
| `ankiConnect.knownWords.maturityEnabled` | `false` | Color known words by card maturity using `subtitleStyle.knownWordMaturityColors` |
|
||||||
|
| `ankiConnect.knownWords.matureThresholdDays` | `21` | Interval in days at which a card counts as mature |
|
||||||
|
| `ankiConnect.nPlusOne.enabled` | `false` | Highlight the only unknown word in a sentence. Needs known-word data |
|
||||||
|
| `ankiConnect.nPlusOne.minSentenceWords` | `3` | Minimum words in a sentence before N+1 applies |
|
||||||
|
|
||||||
|
Use word fields such as `Expression` or `Word` in `knownWords.decks`, not reading fields. See [Subtitle annotations](/subtitle-annotations) for how matching and maturity tiers work.
|
||||||
|
|
||||||
|
### Kiku/Lapis integration {#kiku-lapis-integration}
|
||||||
|
|
||||||
|
Note-type behavior for [Lapis](https://github.com/donkuri/lapis), [Kiku](https://kiku.youyoumu.my.id/), and [Senren](https://github.com/BrenoAqua/Senren). With both Lapis and Kiku on, Kiku handles duplicates and the sentence-card model comes from `isLapis`. Kiku and Senren are mutually exclusive. If both are on, Kiku wins and SubMiner logs a warning. See [Anki integration](/anki-integration) for details.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| -------------------------------------------- | --------------------- | ------------------------------------------------------ |
|
||||||
|
| `ankiConnect.isLapis.enabled` | `false` | Mine dedicated sentence cards (`IsSentenceCard`) |
|
||||||
|
| `ankiConnect.isLapis.sentenceCardModel` | `"Lapis"` | Note type used for sentence cards |
|
||||||
|
| `ankiConnect.isKiku.enabled` | `false` | Merge duplicate word cards |
|
||||||
|
| `ankiConnect.isKiku.fieldGrouping` | `"disabled"` | `auto`, `manual`, or `disabled`. See below |
|
||||||
|
| `ankiConnect.isKiku.deleteDuplicateInAuto` | `true` | Delete the duplicate after an `auto` merge |
|
||||||
|
| `ankiConnect.isSenren.enabled` | `false` | Merge duplicates using Senren's scene-switching format |
|
||||||
|
| `ankiConnect.isSenren.fieldGrouping` | `"auto"` | `auto`, `manual`, or `disabled` |
|
||||||
|
| `ankiConnect.isSenren.deleteDuplicateInAuto` | `true` | Delete the duplicate after an `auto` merge |
|
||||||
|
| `ankiConnect.lapisKiku.wordCardKind` | `"word-and-sentence"` | Card-type flag set on word cards. See below |
|
||||||
|
|
||||||
|
### Word card type
|
||||||
|
|
||||||
|
When SubMiner fills the sentence on a word card, it sets one card-type flag and clears the others. Only applies while `isLapis` or `isKiku` is on. Cards from Mine Sentence and Mine Audio keep their own flag.
|
||||||
|
|
||||||
|
| `wordCardKind` | Flag set |
|
||||||
|
| ----------------------------- | ----------------------- |
|
||||||
|
| `word-and-sentence` (default) | `IsWordAndSentenceCard` |
|
||||||
|
| `click` | `IsClickCard` |
|
||||||
|
| `sentence` | `IsSentenceCard` |
|
||||||
|
| `audio` | `IsAudioCard` |
|
||||||
|
| `none` | none, flags left as-is |
|
||||||
|
|
||||||
|
### Field grouping modes
|
||||||
|
|
||||||
|
| Mode | What happens when you mine a duplicate |
|
||||||
|
| ---------- | ---------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `auto` | Merges the new card into the existing one. `deleteDuplicateInAuto` decides whether the new card is deleted |
|
||||||
|
| `manual` | Pauses playback and opens a dialog to choose which card to keep and whether to delete the other |
|
||||||
|
| `disabled` | Leaves both cards as they are |
|
||||||
|
|
||||||
|
<video controls playsinline preload="metadata" :poster="withBase('/assets/kiku-integration-poster.jpg')" style="width: 100%; max-width: 960px;">
|
||||||
|
<source :src="withBase('/assets/kiku-integration.webm')" type="video/webm" />
|
||||||
|
<source :src="withBase('/assets/kiku-integration.mp4')" type="video/mp4" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
## External integrations
|
||||||
|
|
||||||
|
### Jimaku
|
||||||
|
|
||||||
|
Search and download Japanese subtitles from [Jimaku](https://jimaku.cc). See [Jimaku integration](/jimaku-integration).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| --------------------------- | --------------------- | ---------------------------------------------------------- |
|
||||||
|
| `jimaku.apiKey` | `""` | API key. Optional, but raises your rate limit |
|
||||||
|
| `jimaku.apiKeyCommand` | `""` | Shell command that prints the key. Use instead of `apiKey` |
|
||||||
|
| `jimaku.apiBaseUrl` | `"https://jimaku.cc"` | API base URL |
|
||||||
|
| `jimaku.languagePreference` | `"ja"` | Preferred language: `ja`, `en`, or `none` |
|
||||||
|
| `jimaku.maxEntryResults` | `10` | Maximum search results |
|
||||||
|
|
||||||
|
### TsukiHime
|
||||||
|
|
||||||
|
Subtitle search that needs no account or key. It does need `xz` on your `PATH`. The shortcut is `shortcuts.openTsukihime`. See [TsukiHime integration](/tsukihime-integration).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ---------------------------- | -------------------------------- | ---------------------------------------------------- |
|
||||||
|
| `tsukihime.apiBaseUrl` | `"https://api.tsukihime.org/v1"` | API base URL. Only change it for a mirror |
|
||||||
|
| `tsukihime.maxSearchResults` | `10` | Maximum releases per search (the API caps it at 100) |
|
||||||
|
|
||||||
|
### TMDB
|
||||||
|
|
||||||
|
Posters, synopses, and show grouping for live-action titles in the stats [Library](/immersion-tracking). Release builds include a TMDB key, so you only need your own to use your own quota or when running from source. Get one free under **Settings > API** on [themoviedb.org](https://www.themoviedb.org/settings/api). Either the API key or the read access token works.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| -------------------- | ------- | ---------------------------------------------------------- |
|
||||||
|
| `tmdb.apiKey` | `""` | Your TMDB key or token. Overrides the bundled key |
|
||||||
|
| `tmdb.apiKeyCommand` | `""` | Shell command that prints the key. Use instead of `apiKey` |
|
||||||
|
|
||||||
|
This product uses the TMDB API but is not endorsed or certified by TMDB.
|
||||||
|
|
||||||
|
### Japanese subtitle generation
|
||||||
|
|
||||||
|
Transcribes Japanese subtitles locally with whisper.cpp. Open it with `Ctrl+Shift+G` (`shortcuts.openSubtitleGeneration`) or from the subtitle sidebar. See [Subtitle generation](/subtitle-generation).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| --------------------------------- | --------- | ----------------------------------------------------------------------- |
|
||||||
|
| `subtitleGeneration.modelPath` | `""` | Path to a multilingual whisper.cpp GGML model. Overrides `managedModel` |
|
||||||
|
| `subtitleGeneration.managedModel` | `"small"` | Model SubMiner downloads and uses when `modelPath` is empty |
|
||||||
|
| `subtitleGeneration.threads` | `4` | CPU threads |
|
||||||
|
| `subtitleGeneration.vadModelPath` | `""` | Silero VAD model. Set it to focus on spoken dialogue by default |
|
||||||
|
| `subtitleGeneration.whisperPath` | `""` | `whisper-cli` path. Empty searches `PATH` |
|
||||||
|
| `subtitleGeneration.vadPath` | `""` | Speech detector path. Empty searches `PATH` |
|
||||||
|
| `subtitleGeneration.ffmpegPath` | `""` | `ffmpeg` path. Empty searches `PATH` |
|
||||||
|
| `subtitleGeneration.ffprobePath` | `""` | `ffprobe` path. Empty searches `PATH` |
|
||||||
|
|
||||||
|
### Subtitle sync
|
||||||
|
|
||||||
|
Retimes a subtitle track with [`alass`](https://github.com/kaegi/alass) (against another subtitle or the video) or [`ffsubsync`](https://github.com/smacke/ffsubsync) (against the video's audio). Install them yourself. Open the picker with `Ctrl+Alt+S` (`shortcuts.triggerSubsync`).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------ | ------- | ------------------------------------------------------------------- |
|
||||||
|
| `subsync.alass_path` | `""` | `alass` path. Empty uses `/usr/bin/alass` |
|
||||||
|
| `subsync.ffsubsync_path` | `""` | `ffsubsync` path. Empty uses `/usr/bin/ffsubsync` |
|
||||||
|
| `subsync.ffmpeg_path` | `""` | `ffmpeg` path. Empty uses `/usr/bin/ffmpeg` |
|
||||||
|
| `subsync.replace` | `true` | Overwrite the subtitle file. When off, write `<name>_retimed.<ext>` |
|
||||||
|
|
||||||
|
If a tool lives somewhere else, such as on macOS or Windows, set its path.
|
||||||
|
|
||||||
|
### AniList
|
||||||
|
|
||||||
|
Updates your AniList watch progress after an episode, and controls the character dictionary. With `enabled` on and no token, SubMiner opens a login window. See [AniList integration](/anilist-integration) and [Character dictionary](/character-dictionary).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ---------------------------------------------------------------------- | ------- | ------------------------------------------------------------- |
|
||||||
|
| `anilist.enabled` | `false` | Enable progress updates |
|
||||||
|
| `anilist.accessToken` | `""` | Token override. Empty uses the token saved during login |
|
||||||
|
| `anilist.characterDictionary.maxLoaded` | `3` | How many recent shows stay in the merged character dictionary |
|
||||||
|
| `anilist.characterDictionary.collapsibleSections.description` | `false` | Open the Description section by default |
|
||||||
|
| `anilist.characterDictionary.collapsibleSections.characterInformation` | `false` | Open the Character Information section by default |
|
||||||
|
| `anilist.characterDictionary.collapsibleSections.voicedBy` | `false` | Open the Voiced by section by default |
|
||||||
|
|
||||||
|
### Yomitan
|
||||||
|
|
||||||
|
Point SubMiner at another app's Yomitan Electron profile to reuse its dictionaries and settings. For GameSentenceMiner on Linux this is usually `~/.config/gsm_overlay`.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ----------------------------- | ------- | ----------------------------------------------------------------------- |
|
||||||
|
| `yomitan.externalProfilePath` | `""` | Absolute or `~` path to the external profile. Empty uses SubMiner's own |
|
||||||
|
|
||||||
|
In external-profile mode, SubMiner only reads the profile. It does not open its own Yomitan settings, does not change dictionaries, and turns off all character-dictionary features.
|
||||||
|
|
||||||
|
### Jellyfin
|
||||||
|
|
||||||
|
Log in to a Jellyfin server, browse libraries, and play or cast to SubMiner. Login tokens are stored encrypted, not in this file. See [Jellyfin integration](/jellyfin-integration).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ----------------------------------- | -------------------------------- | ----------------------------------------------- |
|
||||||
|
| `jellyfin.enabled` | `false` | Enable Jellyfin |
|
||||||
|
| `jellyfin.serverUrl` | `""` | Server URL, for example `http://localhost:8096` |
|
||||||
|
| `jellyfin.username` | `""` | Default username for `subminer jellyfin -l` |
|
||||||
|
| `jellyfin.remoteControlEnabled` | `true` | Let Jellyfin apps cast to SubMiner |
|
||||||
|
| `jellyfin.remoteControlAutoConnect` | `true` | Connect the cast session on startup |
|
||||||
|
| `jellyfin.autoAnnounce` | `false` | Announce SubMiner as a cast target on connect |
|
||||||
|
| `jellyfin.pullPictures` | `false` | Fetch posters for launcher pickers |
|
||||||
|
| `jellyfin.iconCacheDir` | `"/tmp/subminer-jellyfin-icons"` | Poster cache folder |
|
||||||
|
| `jellyfin.directPlayPreferred` | `true` | Try direct play before transcoding |
|
||||||
|
| `jellyfin.transcodeVideoCodec` | `"h264"` | Codec requested when transcoding |
|
||||||
|
|
||||||
|
### Discord rich presence
|
||||||
|
|
||||||
|
Shows what you are watching on your Discord profile. Needs the Discord desktop app running. If Discord is closed, SubMiner skips updates.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ---------------------------------- | ----------- | ------------------------------------------------------------------ |
|
||||||
|
| `discordPresence.enabled` | `true` | Enable rich presence |
|
||||||
|
| `discordPresence.presenceStyle` | `"default"` | Card text: `default`, `meme`, `japanese` (all Japanese), `minimal` |
|
||||||
|
| `discordPresence.updateIntervalMs` | `3000` | Minimum ms between updates |
|
||||||
|
| `discordPresence.debounceMs` | `750` | Debounce for bursts of playback events |
|
||||||
|
|
||||||
|
### Immersion tracking
|
||||||
|
|
||||||
|
Records watch sessions, subtitle lines, and mining in a local SQLite database that feeds the stats dashboard. See [Immersion tracking](/immersion-tracking) for retention and storage details. To turn it off for one run, start with `SUBMINER_DISABLE_IMMERSION_TRACKING=1 subminer`.
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------------------------ | ------------ | ----------------------------------------------------------------- |
|
||||||
|
| `immersionTracking.enabled` | `true` | Enable tracking |
|
||||||
|
| `immersionTracking.dbPath` | `""` | Database path. Empty uses `immersion.sqlite` in the config folder |
|
||||||
|
| `immersionTracking.batchSize` | `25` | Writes per transaction |
|
||||||
|
| `immersionTracking.flushIntervalMs` | `500` | Maximum ms before queued writes are saved |
|
||||||
|
| `immersionTracking.queueCap` | `1000` | Queue size. The oldest writes drop when full |
|
||||||
|
| `immersionTracking.payloadCapBytes` | `256` | Maximum event payload size before truncation |
|
||||||
|
| `immersionTracking.maintenanceIntervalMs` | `86400000` | How often pruning and rollups run (24 h) |
|
||||||
|
| `immersionTracking.retentionMode` | `"preset"` | `preset` uses `retentionPreset`. `advanced` uses `retention.*` |
|
||||||
|
| `immersionTracking.retentionPreset` | `"balanced"` | `minimal`, `balanced`, or `deep-history` |
|
||||||
|
| `immersionTracking.retention.eventsDays` | `0` | Days to keep raw events. `0` keeps everything |
|
||||||
|
| `immersionTracking.retention.telemetryDays` | `0` | Days to keep telemetry |
|
||||||
|
| `immersionTracking.retention.sessionsDays` | `0` | Days to keep sessions |
|
||||||
|
| `immersionTracking.retention.dailyRollupsDays` | `0` | Days to keep daily rollups |
|
||||||
|
| `immersionTracking.retention.monthlyRollupsDays` | `0` | Days to keep monthly rollups |
|
||||||
|
| `immersionTracking.retention.vacuumIntervalDays` | `0` | Days between `VACUUM` runs. `0` disables |
|
||||||
|
| `immersionTracking.lifetimeSummaries.global` | `true` | Keep all-time totals |
|
||||||
|
| `immersionTracking.lifetimeSummaries.anime` | `true` | Keep per-show totals |
|
||||||
|
| `immersionTracking.lifetimeSummaries.media` | `true` | Keep per-file totals |
|
||||||
|
|
||||||
|
### Stats dashboard
|
||||||
|
|
||||||
|
A local web dashboard at `http://127.0.0.1:<serverPort>`, also available as an overlay inside SubMiner. It reads the immersion tracking database, so tracking must be on. See [Immersion tracking](/immersion-tracking).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ----------------------- | ------------- | ------------------------------------------------------------------ |
|
||||||
|
| `stats.toggleKey` | `"Backquote"` | Key that toggles the stats overlay (overlay focus only) |
|
||||||
|
| `stats.markWatchedKey` | `"KeyW"` | Key that marks the video watched and plays the next playlist entry |
|
||||||
|
| `stats.serverPort` | `6969` | Dashboard port |
|
||||||
|
| `stats.autoStartServer` | `true` | Start the dashboard server once tracking is active |
|
||||||
|
| `stats.autoOpenBrowser` | `false` | Open the browser when `subminer stats` starts the server |
|
||||||
|
|
||||||
|
### MPV launcher
|
||||||
|
|
||||||
|
Settings for mpv instances that SubMiner starts, and for the bundled mpv plugin. See [mpv plugin](/mpv-plugin).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ---------------------------- | ----------------- | --------------------------------------------------------------------------- |
|
||||||
|
| `mpv.executablePath` | `""` | Path to `mpv.exe` on Windows. Empty checks `SUBMINER_MPV_PATH`, then `PATH` |
|
||||||
|
| `mpv.launchMode` | `"normal"` | Window state: `normal`, `maximized`, or `fullscreen` |
|
||||||
|
| `mpv.profile` | `""` | mpv profile to pass. Combined with a launcher `--profile` if both are set |
|
||||||
|
| `mpv.socketPath` | platform-specific | mpv IPC socket. See the warning under [Config file](#configuration-file) |
|
||||||
|
| `mpv.backend` | `"auto"` | Window tracking: `auto`, `hyprland`, `sway`, `x11`, `macos`, `windows` |
|
||||||
|
| `mpv.autoStartSubMiner` | `true` | Start SubMiner in the background when mpv loads a file |
|
||||||
|
| `mpv.pauseUntilOverlayReady` | `true` | Keep mpv paused until subtitles are ready, up to 30 seconds |
|
||||||
|
| `mpv.subminerBinaryPath` | `""` | SubMiner app path for the plugin. Empty uses the detected path |
|
||||||
|
| `mpv.aniskipEnabled` | `true` | Detect intros with AniSkip and show a skip prompt |
|
||||||
|
| `mpv.aniskipButtonKey` | `"TAB"` | mpv key that skips the intro while the prompt is shown |
|
||||||
|
|
||||||
|
### YouTube playback settings
|
||||||
|
|
||||||
|
Subtitle, language, and card-media settings for YouTube playback. With YouTube captions, SubMiner loads a Japanese primary and English secondary track, preferring manual uploads over auto captions. See [YouTube integration](/youtube-integration).
|
||||||
|
|
||||||
|
| Key | Default | What it does |
|
||||||
|
| ------------------------------ | --------------- | -------------------------------------------------------------------------------------------- |
|
||||||
|
| `youtube.primarySubLanguages` | `["ja", "jpn"]` | Languages that count as a valid primary track, also used for local playback |
|
||||||
|
| `youtube.subtitleSource` | `"youtube"` | `youtube` downloads YouTube's captions. `whisper` transcribes the audio with Whisper |
|
||||||
|
| `youtube.mediaCache.mode` | `"direct"` | `direct` cuts card media from the stream. `background` downloads the video with yt-dlp first |
|
||||||
|
| `youtube.mediaCache.maxHeight` | `720` | Maximum download height in `background` mode. `0` is unlimited |
|
||||||
|
|
||||||
|
Use `background` if card media fails with YouTube `403` errors. Cards mined before the download finishes get their text right away and their audio and image once the file is ready.
|
||||||