mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor(main): eliminate unsafe runtime cast escapes
Tighten main/runtime dependency contracts to remove non-test `as never` and `as unknown as` usage so type drift surfaces during compile/test checks instead of at runtime.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
---
|
||||
id: TASK-105
|
||||
title: Eliminate unsafe non-test runtime casts in main boundaries
|
||||
status: To Do
|
||||
assignee: []
|
||||
status: Done
|
||||
assignee:
|
||||
- opencode-task105-unsafe-casts
|
||||
created_date: '2026-02-22 07:13'
|
||||
updated_date: '2026-02-22 07:13'
|
||||
updated_date: '2026-02-22 21:56'
|
||||
labels:
|
||||
- refactor
|
||||
- type-safety
|
||||
@@ -36,15 +37,43 @@ Current scan shows repeated casts in dependency builders and runtime adapters, w
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Non-test `as never` occurrences in `src/main.ts` and `src/main/runtime` are reduced to zero or documented narrow exceptions.
|
||||
- [ ] #2 Runtime dependency builders compile without unsafe production-path cast escapes.
|
||||
- [ ] #3 Contract regressions are caught by compile/test checks rather than runtime behavior.
|
||||
- [x] #1 Non-test `as never` occurrences in `src/main.ts` and `src/main/runtime` are reduced to zero or documented narrow exceptions.
|
||||
- [x] #2 Runtime dependency builders compile without unsafe production-path cast escapes.
|
||||
- [x] #3 Contract regressions are caught by compile/test checks rather than runtime behavior.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
1) Baseline cast inventory in non-test scope (`src/main.ts`, `src/main/runtime/*`) using `rg` and record before-count in notes.
|
||||
2) Tighten runtime adapter contracts to remove `as never` escapes in `*-main-deps` modules: MPV/Jellyfin defaults, startup config deps, overlay visibility/options deps, field-grouping deps, CLI context deps, subtitle tokenization deps, dictionary runtime deps, tray/app deps, and MPV event bindings.
|
||||
3) Update `src/main.ts` boundary wiring to match tightened contracts and remove cast escapes in config hot-reload setters, dictionary lookup setters, MPV defaults/OSD wiring, MPV client factory constructor typing, tray creation, and overlay bootstrap callbacks.
|
||||
4) Re-run cast scan expecting zero non-test unsafe casts (or document narrow exceptions), run required gates (`bun run build`, `bun run test:core:src`), then attach before/after cast report and finalize AC/DoD.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Executed TASK-105 implementation with runtime contract tightening across `src/main.ts` + `src/main/runtime/*` non-test files. Removed all unsafe `as never` / `as unknown as` casts in target scope by replacing unknown passthrough contracts with typed deps (MPV/Jellyfin runtime types, overlay/bootstrap deps, tokenizer/CLI deps, dictionary lookup function types, tray deps typing).
|
||||
|
||||
Cast reduction report: baseline scan count = 42 non-test matches (`rg -n "as\\s+never|as\\s+unknown\\s+as" src/main.ts src/main/runtime --glob '!**/*.test.ts'`), after changes = 0 matches with same command.
|
||||
|
||||
Verification: targeted runtime cast-refactor tests passed (`bun test ...` across 19 files, 35 pass / 0 fail). Required core lane passed: `bun run test:core:src` => 236 pass / 6 skip / 0 fail.
|
||||
|
||||
Blocker: `bun run build` currently fails on pre-existing unrelated typing issues in `src/anki-integration/note-update-workflow.test.ts` (6 TS errors). No remaining build errors from TASK-105 touched files.
|
||||
|
||||
Follow-up verification rerun after concurrent compile-fix pass: `bun run build` now succeeds on current HEAD (tsc + renderer build + packaging copy step). DoD build gate unblocked.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
Eliminated unsafe non-test runtime cast escapes in `src/main.ts` and `src/main/runtime/*` by tightening runtime dependency-builder contracts (MPV/Jellyfin, overlay/bootstrap, CLI/tokenizer, dictionary/tray boundaries) and removing `as never` / `as unknown as` patterns in production paths. Captured cast reduction evidence (42 -> 0 matches in scoped scan), validated focused runtime suites (35 pass), and revalidated required gates (`bun run test:core:src`, `bun run build`) on current HEAD.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
## Definition of Done
|
||||
<!-- DOD:BEGIN -->
|
||||
- [ ] #1 Cast reduction report attached in task notes (before/after counts).
|
||||
- [ ] #2 `bun run build` and `bun run test:core:src` pass.
|
||||
- [ ] #3 Any remaining exceptions have explicit rationale in code comments or task notes.
|
||||
- [x] #1 Cast reduction report attached in task notes (before/after counts).
|
||||
- [x] #2 `bun run build` and `bun run test:core:src` pass.
|
||||
- [x] #3 Any remaining exceptions have explicit rationale in code comments or task notes.
|
||||
<!-- DOD:END -->
|
||||
|
||||
|
||||
@@ -2,74 +2,82 @@
|
||||
|
||||
Read first. Keep concise.
|
||||
|
||||
| agent_id | alias | mission | status | file | last_update_utc |
|
||||
| ------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------- | ---------------------- |
|
||||
| `codex-aniskip-intro-skip-20260222T080257Z-51fx` | `codex-aniskip-intro-skip` | `Port intro skip to AniSkip API in mpv plugin with OSD skip button` | `handoff` | `docs/subagents/agents/codex-aniskip-intro-skip-20260222T080257Z-51fx.md` | `2026-02-22T10:04:20Z` |
|
||||
| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` |
|
||||
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
||||
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` |
|
||||
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
||||
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` |
|
||||
| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
|
||||
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
|
||||
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |
|
||||
| `codex-texthooker-color-ws-20260220T005844Z-r7m2` | `codex-texthooker-color-ws` | `Fix texthooker websocket payload so token highlight colors render` | `completed` | `docs/subagents/agents/codex-texthooker-color-ws-20260220T005844Z-r7m2.md` | `2026-02-20T01:01:00Z` |
|
||||
| `codex-nplusone-pos1-20260220T012300Z-c5he` | `codex-nplusone-pos1` | `Fix N+1 false-negative when Yomitan functional tokens inflate unknown candidate count` | `completed` | `docs/subagents/agents/codex-nplusone-pos1-20260220T012300Z-c5he.md` | `2026-02-20T01:28:20Z` |
|
||||
| `codex-subtitle-bg-20260220T054247Z-h9cu` | `codex-subtitle-bg` | `Update default subtitle background color to requested RGBA value` | `completed` | `docs/subagents/agents/codex-subtitle-bg-20260220T054247Z-h9cu.md` | `2026-02-20T05:44:45Z` |
|
||||
| `codex-narrow-space-tokenizer-20260220T061716Z-p97s` | `codex-narrow-space-tokenizer` | `Fix tokenization when subtitle line contains narrow/invisible Unicode spacing between segments` | `completed` | `docs/subagents/agents/codex-narrow-space-tokenizer-20260220T061716Z-p97s.md` | `2026-02-20T06:20:07Z` |
|
||||
| `codex-preserve-linebreaks-20260220T063538Z-s4nd` | `codex-preserve-linebreaks` | `Add config option to preserve subtitle line breaks in visible overlay rendering` | `completed` | `docs/subagents/agents/codex-preserve-linebreaks-20260220T063538Z-s4nd.md` | `2026-02-20T06:42:51Z` |
|
||||
| `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` |
|
||||
| `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` |
|
||||
| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` |
|
||||
| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` |
|
||||
| `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` |
|
||||
| `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` |
|
||||
| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-21T02:04:12Z` |
|
||||
| `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` |
|
||||
| `codex-task95-hotspots-20260221T031420Z-x7k2` | `codex-task95-hotspots` | `Execute TASK-95 decompose oversized core hotspots end-to-end` | `done` | `docs/subagents/agents/codex-task95-hotspots-20260221T031420Z-x7k2.md` | `2026-02-21T03:29:23Z` |
|
||||
| `codex-task94-thin-root-20260221T031320Z-q3n7` | `codex-task94-thin-root` | `Execute TASK-94 by extracting main.ts deps-builder clusters into runtime composers` | `handoff` | `docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md` | `2026-02-21T03:41:10Z` |
|
||||
| `opencode-task95-immersion-tracker-20260221T031846Z-p4k9` | `opencode-task95-immersion-tracker` | `Implement TASK-95 immersion-tracker extraction into focused collaborators and seam tests` | `handoff` | `docs/subagents/agents/opencode-task95-immersion-tracker-20260221T031846Z-p4k9.md` | `2026-02-21T03:26:51Z` |
|
||||
| `opencode-task95-config-20260221T031843Z-m4k9` | `opencode-task95-config` | `Implement TASK-95 config extraction for src/config/service.ts` | `done` | `docs/subagents/agents/opencode-task95-config-20260221T031843Z-m4k9.md` | `2026-02-21T03:26:57Z` |
|
||||
| `codex-task95-anki-20260221T031836Z-6f3e` | `codex-task95-anki` | `Implement TASK-95 anki-integration extraction for field-grouping merge collaborator` | `done` | `docs/subagents/agents/codex-task95-anki-20260221T031836Z-6f3e.md` | `2026-02-21T03:26:55Z` |
|
||||
| `opencode-task-94-20260221T033647Z-7ou2` | `opencode-task-94` | `Finish TASK-94 thin composition root refactor and close acceptance criteria` | `done` | `docs/subagents/agents/opencode-task-94-20260221T033647Z-7ou2.md` | `2026-02-21T04:12:45Z` |
|
||||
| `codex-task71-round2-20260221T043541Z-k9t3` | `codex-task71-round2` | `Execute TASK-71 round 2 split of main.ts into domain runtime modules` | `done` | `docs/subagents/agents/codex-task71-round2-20260221T043541Z-k9t3.md` | `2026-02-21T04:57:00Z` |
|
||||
| `codex-task85-20260221T051308Z-164g` | `codex-task85-exec` | `Execute TASK-85 remaining AC/DoD with writing-plans and executing-plans flow` | `done` | `docs/subagents/agents/codex-task85-20260221T051308Z-164g.md` | `2026-02-21T05:45:50Z` |
|
||||
| `codex-review-refactor-20260221T062353Z-p6k2` | `codex-review-refactor` | `Perform code review for current refactor changes and report actionable findings` | `done` | `docs/subagents/agents/codex-review-refactor-20260221T062353Z-p6k2.md` | `2026-02-21T07:16:33Z` |
|
||||
| `opencode-task93-sync-20260221T070842Z-71c6` | `opencode-task93-sync` | `Synchronize TASK-85 closure tracking and child-task status in Backlog` | `done` | `docs/subagents/agents/opencode-task93-sync-20260221T070842Z-71c6.md` | `2026-02-21T07:11:58Z` |
|
||||
| `opencode-task97-runtime-composer-20260221T094150Z-r8k3` | `opencode-task97-runtime-composer` | `Execute TASK-97 normalize runtime composer contracts end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task97-runtime-composer-20260221T094150Z-r8k3.md` | `2026-02-21T10:06:59Z` |
|
||||
| `opencode-task96-config-resolve-20260221T094119Z-mbfo` | `opencode-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules with plan-first workflow` | `planning` | `docs/subagents/agents/opencode-task96-config-resolve-20260221T094119Z-mbfo.md` | `2026-02-21T09:41:19Z` |
|
||||
| `opencode-task98-source-tests-20260221T094524Z-kzvd` | `opencode-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling without commit` | `blocked` | `docs/subagents/agents/opencode-task98-source-tests-20260221T094524Z-kzvd.md` | `2026-02-21T09:56:47Z` |
|
||||
| `codex-task96-config-resolve-20260221T110058Z-k7m2` | `codex-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules end-to-end without commit` | `done` | `docs/subagents/agents/codex-task96-config-resolve-20260221T110058Z-k7m2.md` | `2026-02-21T20:10:43Z` |
|
||||
| `codex-task73-mpv-socket-20260221T201605Z-zjhs` | `codex-task73-mpv-socket` | `Execute TASK-73 consolidate launcher mpv socket readiness primitives end-to-end` | `done` | `docs/subagents/agents/codex-task73-mpv-socket-20260221T201605Z-zjhs.md` | `2026-02-21T20:20:18Z` |
|
||||
| `codex-task74-launcher-tests-20260221T201635Z-10i6` | `codex-task74-launcher-tests` | `Implement TASK-74 launcher regression tests for config discovery + command branching end-to-end` | `done` | `docs/subagents/agents/codex-task74-launcher-tests-20260221T201635Z-10i6.md` | `2026-02-21T20:20:52Z` |
|
||||
| `opencode-task76-anki-workflows-20260221T201659Z-r4p1` | `opencode-task76-anki-workflows` | `Execute TASK-76 decompose anki-integration orchestrator into workflow services via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task76-anki-workflows-20260221T201659Z-r4p1.md` | `2026-02-21T21:17:28Z` |
|
||||
| `opencode-task76-doc-boundaries-20260221T203558Z-h7q4` | `opencode-task76-doc-boundaries` | `Update Anki integration docs with post-decomposition ownership boundaries for TASK-76` | `done` | `docs/subagents/agents/opencode-task76-doc-boundaries-20260221T203558Z-h7q4.md` | `2026-02-21T20:36:55Z` |
|
||||
| `codex-docs-unpushed-review-20260221T213707Z-lyej` | `codex-docs-unpushed-review` | `Review unpushed commits for docs drift; patch docs to reflect current code/state` | `done` | `docs/subagents/agents/codex-docs-unpushed-review-20260221T213707Z-lyej.md` | `2026-02-21T21:39:15Z` |
|
||||
| `codex-task72-strict-startup-config-20260221T231804Z-3ngd` | `codex-task72-strict-startup-config` | `Execute TASK-72 strict startup config loading with actionable user-facing errors` | `done` | `docs/subagents/agents/codex-task72-strict-startup-config-20260221T231804Z-3ngd.md` | `2026-02-21T23:26:29Z` |
|
||||
| `opencode-task77-tokenizer-stages-20260221T232016Z-v9k2` | `opencode-task77-tokenizer-stages` | `Execute TASK-77 tokenizer pipeline split into parser-selection enrichment and annotation stages without commit` | `done` | `docs/subagents/agents/opencode-task77-tokenizer-stages-20260221T232016Z-v9k2.md` | `2026-02-21T23:47:08Z` |
|
||||
| `codex-task75-mpv-osd-buffered-20260221T231816Z-yj32` | `codex-task75-mpv-osd-buffered` | `Execute TASK-75 move MPV OSD log writes to buffered async path end-to-end` | `done` | `docs/subagents/agents/codex-task75-mpv-osd-buffered-20260221T231816Z-yj32.md` | `2026-02-21T23:48:10Z` |
|
||||
| `opencode-task72-strict-startup-config-20260221T232155Z-kf0o` | `opencode-task72-strict-startup-config` | `Implement Task 1 from strict startup config loading plan with startup malformed-config failure signal and tests` | `done` | `docs/subagents/agents/opencode-task72-strict-startup-config-20260221T232155Z-kf0o.md` | `2026-02-21T23:24:32Z` |
|
||||
| `opencode-task72-parse-details-20260221T232137Z-b63t` | `opencode-task72-parse-details` | `Implement TASK-72 Task 2 shared parse-error formatter wiring and tests` | `done` | `docs/subagents/agents/opencode-task72-parse-details-20260221T232137Z-b63t.md` | `2026-02-21T23:24:12Z` |
|
||||
| `opencode-task77-slice-a-20260222T000100Z-j4p2` | `opencode-task77-slice-a` | `Implement TASK-77 slice A parser-selection-stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-slice-a-20260222T000100Z-j4p2.md` | `2026-02-22T00:03:30Z` |
|
||||
| `opencode-task78-config-domain-20260221T235604Z-p9x2` | `opencode-task78-config-domain` | `Execute TASK-78 modularize config definitions and validation by domain end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task78-config-domain-20260221T235604Z-p9x2.md` | `2026-02-22T00:06:30Z` |
|
||||
| `opencode-task77-sliceb-20260221T232507Z-vzk5` | `opencode-task77-sliceb` | `Implement TASK-77 slice B parser-enrichment stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-sliceb-20260221T232507Z-vzk5.md` | `2026-02-21T23:27:40Z` |
|
||||
| `opencode-task79-runtime-reducers-20260221T235652Z-n4p7` | `opencode-task79-runtime-reducers` | `Execute TASK-79 explicit runtime state transitions/reducers in main via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task79-runtime-reducers-20260221T235652Z-n4p7.md` | `2026-02-22T00:10:51Z` |
|
||||
| `opencode-task79-sliceb-20260222T000253Z-m2r7` | `opencode-task79-sliceb` | `Implement TASK-79 slice B invariants coverage/tests and composition-boundary docs updates without commit` | `done` | `docs/subagents/agents/opencode-task79-sliceb-20260222T000253Z-m2r7.md` | `2026-02-22T00:04:21Z` |
|
||||
| `opencode-task80-ipc-contract-20260222T001728Z-obrv` | `opencode-task80-ipc-contract` | `Execute TASK-80 IPC contract typing + runtime payload validation end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task80-ipc-contract-20260222T001728Z-obrv.md` | `2026-02-22T00:56:00Z` |
|
||||
| `opencode-task82-smoke-20260222T002150Z-p5bp` | `opencode-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task82-smoke-20260222T002150Z-p5bp.md` | `2026-02-22T00:54:29Z` |
|
||||
| `codex-task82-smoke-20260222T002523Z-3j7u` | `codex-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/codex-task82-smoke-20260222T002523Z-3j7u.md` | `2026-02-22T00:53:25Z` |
|
||||
| `opencode-task81-launcher-modules-20260222T005725Z-8oh8` | `opencode-task81-launcher-modules` | `Execute TASK-81 launcher command-module/process-adapter refactor end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task81-launcher-modules-20260222T005725Z-8oh8.md` | `2026-02-22T01:09:30Z` |
|
||||
| `codex-task99-guardrails-20260222T010930Z-m9q2` | `codex-task99-guardrails` | `Execute TASK-99 maintainability guardrail expansion and runtime cycle checks end-to-end without commit` | `done` | `docs/subagents/agents/codex-task99-guardrails-20260222T010930Z-m9q2.md` | `2026-02-22T03:01:34Z` |
|
||||
| `opencode-task84-keybindings-gating-20260222T011624Z-llor` | `opencode-task84-keybindings-gating` | `Execute TASK-84 gate feature-dependent keybindings behind config flags end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task84-keybindings-gating-20260222T011624Z-llor.md` | `2026-02-22T01:35:30Z` |
|
||||
| `codex-task98-source-tests-20260222T021156Z-a1b2` | `codex-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling end-to-end without commit` | `done` | `docs/subagents/agents/codex-task98-source-tests-20260222T021156Z-a1b2.md` | `2026-02-22T02:36:00Z` |
|
||||
| `codex-task101-docs-archive-20260222T024156Z-hneu` | `codex-task101-docs-archive` | `Execute TASK-101 consolidate architecture docs and archive task-noise evidence` | `done` | `docs/subagents/agents/codex-task101-docs-archive-20260222T024156Z-hneu.md` | `2026-02-22T03:01:38Z` |
|
||||
| `codex-fix-ci-20260222T025848Z-0xdl` | `codex-fix-ci` | `Inspect failing GitHub Actions PR checks and implement approved fix` | `done` | `docs/subagents/agents/codex-fix-ci-20260222T025848Z-0xdl.md` | `2026-02-22T03:25:40Z` |
|
||||
| `opencode-task100-dead-code-prune-20260222T033155Z-qenz` | `opencode-task100-dead-code-prune` | `Execute TASK-100 dead-code prune and cleanup via writing-plans + executing-plans (no commit)` | `done` | `docs/subagents/agents/opencode-task100-dead-code-prune-20260222T033155Z-qenz.md` | `2026-02-22T04:00:41Z` |
|
||||
| `codex-gh-fix-ci-20260222T054631Z-m4t8` | `codex-gh-fix-ci` | `Inspect failing GitHub Actions checks; propose fix plan before implementation` | `done` | `docs/subagents/agents/codex-gh-fix-ci-20260222T054631Z-m4t8.md` | `2026-02-22T06:11:15Z` |
|
||||
| `codex-fix-rebase-errors-20260222T062235Z-73h4` | `codex-fix-rebase-errors` | `Resolve current git rebase conflicts in ipc/main runtime files and land clean rebase state` | `done` | `docs/subagents/agents/codex-fix-rebase-errors-20260222T062235Z-73h4.md` | `2026-02-22T06:30:48Z` |
|
||||
| `codex-review-cleanup-20260222T065718Z-9p4m` | `codex-review-cleanup` | `Review post-refactor codebase quality and create cleanup tickets with concrete scope and completion criteria` | `done` | `docs/subagents/agents/codex-review-cleanup-20260222T065718Z-9p4m.md` | `2026-02-22T07:04:48Z` |
|
||||
| `codex-jellyfin-ts-fix-20260222T071530Z-5e50` | `codex-jellyfin-ts-fix` | `Fix Jellyfin token/session type drift causing TS compile failures in config+main.` | `done` | `docs/subagents/agents/codex-jellyfin-ts-fix-20260222T071530Z-5e50.md` | `2026-02-22T07:23:47Z` |
|
||||
| `codex-overlay-toggle-regression-20260222T073450Z-q7m4` | `codex-overlay-toggle-regression` | `Fix post-rebase overlay toggle regression causing transparent non-interactable windows and broken keybinds (TASK-107).` | `testing` | `docs/subagents/agents/codex-overlay-toggle-regression-20260222T073450Z-q7m4.md` | `2026-02-22T07:45:58Z` |
|
||||
| `codex-docs-review-20260222T094009Z-g8p2` | `codex-docs-review` | `Review README/docs for drift vs current code/scripts; patch stale or missing documentation.` | `done` | `docs/subagents/agents/codex-docs-review-20260222T094009Z-g8p2.md` | `2026-02-22T09:43:52Z` |
|
||||
| agent_id | alias | mission | status | file | last_update_utc |
|
||||
| ------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------- | -------------------------------------------------------------------------------------- | ---------------------- |
|
||||
| `codex-field-grouping-autoupdate-race-20260222T193915Z-m8p4` | `codex-field-grouping-autoupdate-race` | `Fix Kiku field-grouping merge race with auto-update so note enrichment completes before duplicate merge` | `handoff` | `docs/subagents/agents/codex-field-grouping-autoupdate-race-20260222T193915Z-m8p4.md` | `2026-02-22T19:41:20Z` |
|
||||
| `codex-aniskip-intro-skip-20260222T080257Z-51fx` | `codex-aniskip-intro-skip` | `Port intro skip to AniSkip API in mpv plugin with OSD skip button` | `handoff` | `docs/subagents/agents/codex-aniskip-intro-skip-20260222T080257Z-51fx.md` | `2026-02-22T10:04:20Z` |
|
||||
| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` |
|
||||
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
||||
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` |
|
||||
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
||||
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` |
|
||||
| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
|
||||
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
|
||||
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |
|
||||
| `codex-texthooker-color-ws-20260220T005844Z-r7m2` | `codex-texthooker-color-ws` | `Fix texthooker websocket payload so token highlight colors render` | `completed` | `docs/subagents/agents/codex-texthooker-color-ws-20260220T005844Z-r7m2.md` | `2026-02-20T01:01:00Z` |
|
||||
| `codex-nplusone-pos1-20260220T012300Z-c5he` | `codex-nplusone-pos1` | `Fix N+1 false-negative when Yomitan functional tokens inflate unknown candidate count` | `completed` | `docs/subagents/agents/codex-nplusone-pos1-20260220T012300Z-c5he.md` | `2026-02-20T01:28:20Z` |
|
||||
| `codex-subtitle-bg-20260220T054247Z-h9cu` | `codex-subtitle-bg` | `Update default subtitle background color to requested RGBA value` | `completed` | `docs/subagents/agents/codex-subtitle-bg-20260220T054247Z-h9cu.md` | `2026-02-20T05:44:45Z` |
|
||||
| `codex-narrow-space-tokenizer-20260220T061716Z-p97s` | `codex-narrow-space-tokenizer` | `Fix tokenization when subtitle line contains narrow/invisible Unicode spacing between segments` | `completed` | `docs/subagents/agents/codex-narrow-space-tokenizer-20260220T061716Z-p97s.md` | `2026-02-20T06:20:07Z` |
|
||||
| `codex-preserve-linebreaks-20260220T063538Z-s4nd` | `codex-preserve-linebreaks` | `Add config option to preserve subtitle line breaks in visible overlay rendering` | `completed` | `docs/subagents/agents/codex-preserve-linebreaks-20260220T063538Z-s4nd.md` | `2026-02-20T06:42:51Z` |
|
||||
| `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` |
|
||||
| `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` |
|
||||
| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` |
|
||||
| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` |
|
||||
| `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` |
|
||||
| `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` |
|
||||
| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-21T02:04:12Z` |
|
||||
| `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` |
|
||||
| `codex-task95-hotspots-20260221T031420Z-x7k2` | `codex-task95-hotspots` | `Execute TASK-95 decompose oversized core hotspots end-to-end` | `done` | `docs/subagents/agents/codex-task95-hotspots-20260221T031420Z-x7k2.md` | `2026-02-21T03:29:23Z` |
|
||||
| `codex-task94-thin-root-20260221T031320Z-q3n7` | `codex-task94-thin-root` | `Execute TASK-94 by extracting main.ts deps-builder clusters into runtime composers` | `handoff` | `docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md` | `2026-02-21T03:41:10Z` |
|
||||
| `opencode-task95-immersion-tracker-20260221T031846Z-p4k9` | `opencode-task95-immersion-tracker` | `Implement TASK-95 immersion-tracker extraction into focused collaborators and seam tests` | `handoff` | `docs/subagents/agents/opencode-task95-immersion-tracker-20260221T031846Z-p4k9.md` | `2026-02-21T03:26:51Z` |
|
||||
| `opencode-task95-config-20260221T031843Z-m4k9` | `opencode-task95-config` | `Implement TASK-95 config extraction for src/config/service.ts` | `done` | `docs/subagents/agents/opencode-task95-config-20260221T031843Z-m4k9.md` | `2026-02-21T03:26:57Z` |
|
||||
| `codex-task95-anki-20260221T031836Z-6f3e` | `codex-task95-anki` | `Implement TASK-95 anki-integration extraction for field-grouping merge collaborator` | `done` | `docs/subagents/agents/codex-task95-anki-20260221T031836Z-6f3e.md` | `2026-02-21T03:26:55Z` |
|
||||
| `opencode-task-94-20260221T033647Z-7ou2` | `opencode-task-94` | `Finish TASK-94 thin composition root refactor and close acceptance criteria` | `done` | `docs/subagents/agents/opencode-task-94-20260221T033647Z-7ou2.md` | `2026-02-21T04:12:45Z` |
|
||||
| `codex-task71-round2-20260221T043541Z-k9t3` | `codex-task71-round2` | `Execute TASK-71 round 2 split of main.ts into domain runtime modules` | `done` | `docs/subagents/agents/codex-task71-round2-20260221T043541Z-k9t3.md` | `2026-02-21T04:57:00Z` |
|
||||
| `codex-task85-20260221T051308Z-164g` | `codex-task85-exec` | `Execute TASK-85 remaining AC/DoD with writing-plans and executing-plans flow` | `done` | `docs/subagents/agents/codex-task85-20260221T051308Z-164g.md` | `2026-02-21T05:45:50Z` |
|
||||
| `codex-review-refactor-20260221T062353Z-p6k2` | `codex-review-refactor` | `Perform code review for current refactor changes and report actionable findings` | `done` | `docs/subagents/agents/codex-review-refactor-20260221T062353Z-p6k2.md` | `2026-02-21T07:16:33Z` |
|
||||
| `opencode-task93-sync-20260221T070842Z-71c6` | `opencode-task93-sync` | `Synchronize TASK-85 closure tracking and child-task status in Backlog` | `done` | `docs/subagents/agents/opencode-task93-sync-20260221T070842Z-71c6.md` | `2026-02-21T07:11:58Z` |
|
||||
| `opencode-task97-runtime-composer-20260221T094150Z-r8k3` | `opencode-task97-runtime-composer` | `Execute TASK-97 normalize runtime composer contracts end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task97-runtime-composer-20260221T094150Z-r8k3.md` | `2026-02-21T10:06:59Z` |
|
||||
| `opencode-task96-config-resolve-20260221T094119Z-mbfo` | `opencode-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules with plan-first workflow` | `planning` | `docs/subagents/agents/opencode-task96-config-resolve-20260221T094119Z-mbfo.md` | `2026-02-21T09:41:19Z` |
|
||||
| `opencode-task98-source-tests-20260221T094524Z-kzvd` | `opencode-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling without commit` | `blocked` | `docs/subagents/agents/opencode-task98-source-tests-20260221T094524Z-kzvd.md` | `2026-02-21T09:56:47Z` |
|
||||
| `codex-task96-config-resolve-20260221T110058Z-k7m2` | `codex-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules end-to-end without commit` | `done` | `docs/subagents/agents/codex-task96-config-resolve-20260221T110058Z-k7m2.md` | `2026-02-21T20:10:43Z` |
|
||||
| `codex-task73-mpv-socket-20260221T201605Z-zjhs` | `codex-task73-mpv-socket` | `Execute TASK-73 consolidate launcher mpv socket readiness primitives end-to-end` | `done` | `docs/subagents/agents/codex-task73-mpv-socket-20260221T201605Z-zjhs.md` | `2026-02-21T20:20:18Z` |
|
||||
| `codex-task74-launcher-tests-20260221T201635Z-10i6` | `codex-task74-launcher-tests` | `Implement TASK-74 launcher regression tests for config discovery + command branching end-to-end` | `done` | `docs/subagents/agents/codex-task74-launcher-tests-20260221T201635Z-10i6.md` | `2026-02-21T20:20:52Z` |
|
||||
| `opencode-task76-anki-workflows-20260221T201659Z-r4p1` | `opencode-task76-anki-workflows` | `Execute TASK-76 decompose anki-integration orchestrator into workflow services via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task76-anki-workflows-20260221T201659Z-r4p1.md` | `2026-02-21T21:17:28Z` |
|
||||
| `opencode-task76-doc-boundaries-20260221T203558Z-h7q4` | `opencode-task76-doc-boundaries` | `Update Anki integration docs with post-decomposition ownership boundaries for TASK-76` | `done` | `docs/subagents/agents/opencode-task76-doc-boundaries-20260221T203558Z-h7q4.md` | `2026-02-21T20:36:55Z` |
|
||||
| `codex-docs-unpushed-review-20260221T213707Z-lyej` | `codex-docs-unpushed-review` | `Review unpushed commits for docs drift; patch docs to reflect current code/state` | `done` | `docs/subagents/agents/codex-docs-unpushed-review-20260221T213707Z-lyej.md` | `2026-02-21T21:39:15Z` |
|
||||
| `codex-task72-strict-startup-config-20260221T231804Z-3ngd` | `codex-task72-strict-startup-config` | `Execute TASK-72 strict startup config loading with actionable user-facing errors` | `done` | `docs/subagents/agents/codex-task72-strict-startup-config-20260221T231804Z-3ngd.md` | `2026-02-21T23:26:29Z` |
|
||||
| `opencode-task77-tokenizer-stages-20260221T232016Z-v9k2` | `opencode-task77-tokenizer-stages` | `Execute TASK-77 tokenizer pipeline split into parser-selection enrichment and annotation stages without commit` | `done` | `docs/subagents/agents/opencode-task77-tokenizer-stages-20260221T232016Z-v9k2.md` | `2026-02-21T23:47:08Z` |
|
||||
| `codex-task75-mpv-osd-buffered-20260221T231816Z-yj32` | `codex-task75-mpv-osd-buffered` | `Execute TASK-75 move MPV OSD log writes to buffered async path end-to-end` | `done` | `docs/subagents/agents/codex-task75-mpv-osd-buffered-20260221T231816Z-yj32.md` | `2026-02-21T23:48:10Z` |
|
||||
| `opencode-task72-strict-startup-config-20260221T232155Z-kf0o` | `opencode-task72-strict-startup-config` | `Implement Task 1 from strict startup config loading plan with startup malformed-config failure signal and tests` | `done` | `docs/subagents/agents/opencode-task72-strict-startup-config-20260221T232155Z-kf0o.md` | `2026-02-21T23:24:32Z` |
|
||||
| `opencode-task72-parse-details-20260221T232137Z-b63t` | `opencode-task72-parse-details` | `Implement TASK-72 Task 2 shared parse-error formatter wiring and tests` | `done` | `docs/subagents/agents/opencode-task72-parse-details-20260221T232137Z-b63t.md` | `2026-02-21T23:24:12Z` |
|
||||
| `opencode-task77-slice-a-20260222T000100Z-j4p2` | `opencode-task77-slice-a` | `Implement TASK-77 slice A parser-selection-stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-slice-a-20260222T000100Z-j4p2.md` | `2026-02-22T00:03:30Z` |
|
||||
| `opencode-task78-config-domain-20260221T235604Z-p9x2` | `opencode-task78-config-domain` | `Execute TASK-78 modularize config definitions and validation by domain end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task78-config-domain-20260221T235604Z-p9x2.md` | `2026-02-22T00:06:30Z` |
|
||||
| `opencode-task77-sliceb-20260221T232507Z-vzk5` | `opencode-task77-sliceb` | `Implement TASK-77 slice B parser-enrichment stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-sliceb-20260221T232507Z-vzk5.md` | `2026-02-21T23:27:40Z` |
|
||||
| `opencode-task79-runtime-reducers-20260221T235652Z-n4p7` | `opencode-task79-runtime-reducers` | `Execute TASK-79 explicit runtime state transitions/reducers in main via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task79-runtime-reducers-20260221T235652Z-n4p7.md` | `2026-02-22T00:10:51Z` |
|
||||
| `opencode-task79-sliceb-20260222T000253Z-m2r7` | `opencode-task79-sliceb` | `Implement TASK-79 slice B invariants coverage/tests and composition-boundary docs updates without commit` | `done` | `docs/subagents/agents/opencode-task79-sliceb-20260222T000253Z-m2r7.md` | `2026-02-22T00:04:21Z` |
|
||||
| `opencode-task80-ipc-contract-20260222T001728Z-obrv` | `opencode-task80-ipc-contract` | `Execute TASK-80 IPC contract typing + runtime payload validation end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task80-ipc-contract-20260222T001728Z-obrv.md` | `2026-02-22T00:56:00Z` |
|
||||
| `opencode-task82-smoke-20260222T002150Z-p5bp` | `opencode-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task82-smoke-20260222T002150Z-p5bp.md` | `2026-02-22T00:54:29Z` |
|
||||
| `codex-task82-smoke-20260222T002523Z-3j7u` | `codex-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/codex-task82-smoke-20260222T002523Z-3j7u.md` | `2026-02-22T00:53:25Z` |
|
||||
| `opencode-task81-launcher-modules-20260222T005725Z-8oh8` | `opencode-task81-launcher-modules` | `Execute TASK-81 launcher command-module/process-adapter refactor end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task81-launcher-modules-20260222T005725Z-8oh8.md` | `2026-02-22T01:09:30Z` |
|
||||
| `codex-task99-guardrails-20260222T010930Z-m9q2` | `codex-task99-guardrails` | `Execute TASK-99 maintainability guardrail expansion and runtime cycle checks end-to-end without commit` | `done` | `docs/subagents/agents/codex-task99-guardrails-20260222T010930Z-m9q2.md` | `2026-02-22T03:01:34Z` |
|
||||
| `opencode-task84-keybindings-gating-20260222T011624Z-llor` | `opencode-task84-keybindings-gating` | `Execute TASK-84 gate feature-dependent keybindings behind config flags end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task84-keybindings-gating-20260222T011624Z-llor.md` | `2026-02-22T01:35:30Z` |
|
||||
| `codex-task98-source-tests-20260222T021156Z-a1b2` | `codex-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling end-to-end without commit` | `done` | `docs/subagents/agents/codex-task98-source-tests-20260222T021156Z-a1b2.md` | `2026-02-22T02:36:00Z` |
|
||||
| `codex-task101-docs-archive-20260222T024156Z-hneu` | `codex-task101-docs-archive` | `Execute TASK-101 consolidate architecture docs and archive task-noise evidence` | `done` | `docs/subagents/agents/codex-task101-docs-archive-20260222T024156Z-hneu.md` | `2026-02-22T03:01:38Z` |
|
||||
| `codex-fix-ci-20260222T025848Z-0xdl` | `codex-fix-ci` | `Inspect failing GitHub Actions PR checks and implement approved fix` | `done` | `docs/subagents/agents/codex-fix-ci-20260222T025848Z-0xdl.md` | `2026-02-22T03:25:40Z` |
|
||||
| `opencode-task100-dead-code-prune-20260222T033155Z-qenz` | `opencode-task100-dead-code-prune` | `Execute TASK-100 dead-code prune and cleanup via writing-plans + executing-plans (no commit)` | `done` | `docs/subagents/agents/opencode-task100-dead-code-prune-20260222T033155Z-qenz.md` | `2026-02-22T04:00:41Z` |
|
||||
| `codex-gh-fix-ci-20260222T054631Z-m4t8` | `codex-gh-fix-ci` | `Inspect failing GitHub Actions checks; propose fix plan before implementation` | `done` | `docs/subagents/agents/codex-gh-fix-ci-20260222T054631Z-m4t8.md` | `2026-02-22T06:11:15Z` |
|
||||
| `codex-fix-rebase-errors-20260222T062235Z-73h4` | `codex-fix-rebase-errors` | `Resolve current git rebase conflicts in ipc/main runtime files and land clean rebase state` | `done` | `docs/subagents/agents/codex-fix-rebase-errors-20260222T062235Z-73h4.md` | `2026-02-22T06:30:48Z` |
|
||||
| `codex-review-cleanup-20260222T065718Z-9p4m` | `codex-review-cleanup` | `Review post-refactor codebase quality and create cleanup tickets with concrete scope and completion criteria` | `done` | `docs/subagents/agents/codex-review-cleanup-20260222T065718Z-9p4m.md` | `2026-02-22T07:04:48Z` |
|
||||
| `codex-jellyfin-ts-fix-20260222T071530Z-5e50` | `codex-jellyfin-ts-fix` | `Fix Jellyfin token/session type drift causing TS compile failures in config+main.` | `done` | `docs/subagents/agents/codex-jellyfin-ts-fix-20260222T071530Z-5e50.md` | `2026-02-22T07:23:47Z` |
|
||||
| `codex-overlay-toggle-regression-20260222T073450Z-q7m4` | `codex-overlay-toggle-regression` | `Fix post-rebase overlay toggle regression causing transparent non-interactable windows and broken keybinds (TASK-107).` | `testing` | `docs/subagents/agents/codex-overlay-toggle-regression-20260222T073450Z-q7m4.md` | `2026-02-22T07:45:58Z` |
|
||||
| `codex-docs-review-20260222T094009Z-g8p2` | `codex-docs-review` | `Review README/docs for drift vs current code/scripts; patch stale or missing documentation.` | `done` | `docs/subagents/agents/codex-docs-review-20260222T094009Z-g8p2.md` | `2026-02-22T09:43:52Z` |
|
||||
| `codex-discord-presence-task-20260222T194048Z-d7k2` | `codex-discord-presence-task` | `Add backlog task for Discord Rich Presence integration with polished activity card` | `done` | `docs/subagents/agents/codex-discord-presence-task-20260222T194048Z-d7k2.md` | `2026-02-22T19:41:00Z` |
|
||||
| `codex-task108-aniskip-20260222T194600Z-qgdt` | `codex-task108-aniskip` | `Execute TASK-108 end-to-end with plan-first workflow and no commit` | `done` | `docs/subagents/agents/codex-task108-aniskip-20260222T194600Z-qgdt.md` | `2026-02-22T19:49:30Z` |
|
||||
|
||||
| `codex-gh-fix-ci-20260222T191948Z-b7n4` | `codex-gh-fix-ci` | `Triage failing GitHub Actions checks on active PR; summarize root cause; propose fix plan before edits` | `handoff` | `docs/subagents/agents/codex-gh-fix-ci-20260222T191948Z-b7n4.md` | `2026-02-22T19:24:12Z` |
|
||||
| `opencode-task105-unsafe-casts-20260222T194704Z-zfcm` | `opencode-task105-unsafe-casts` | `Execute TASK-105 eliminate unsafe non-test runtime casts in main boundaries end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task105-unsafe-casts-20260222T194704Z-zfcm.md` | `2026-02-22T21:56:30Z` |
|
||||
| `codex-task104-launcher-config-20260222T194708Z-z9x1` | `codex-task104-launcher-config` | `Execute TASK-104 end-to-end with plan-first workflow and no commit` | `done` | `docs/subagents/agents/codex-task104-launcher-config-20260222T194708Z-z9x1.md` | `2026-02-22T19:56:26Z` |
|
||||
| `opencode-task106-immersion-modules-20260222T195109Z-r3m7` | `opencode-task106-immersion-modules` | `Execute TASK-106 decomposition of immersion tracker service into storage session and metadata modules end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task106-immersion-modules-20260222T195109Z-r3m7.md` | `2026-02-22T21:58:45Z` |
|
||||
| `codex-task105-sliceb-20260222T195423Z-w8n3` | `codex-task105-sliceb` | `Implement TASK-105 slice B runtime cast removal in targeted main/runtime modules without commit` | `done` | `docs/subagents/agents/codex-task105-sliceb-20260222T195423Z-w8n3.md` | `2026-02-22T20:02:06Z` |
|
||||
| `codex-ts-build-errors-20260222T215411Z-h3k7` | `codex-ts-build-errors` | `Fix current TypeScript build failures in anki/runtime tests and deps typing contracts; keep behavior unchanged.` | `done` | `docs/subagents/agents/codex-ts-build-errors-20260222T215411Z-h3k7.md` | `2026-02-22T21:55:54Z` |
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Agent Session: opencode-task105-unsafe-casts-20260222T194704Z-zfcm
|
||||
|
||||
- alias: `opencode-task105-unsafe-casts`
|
||||
- mission: `Execute TASK-105 eliminate unsafe non-test runtime casts in main boundaries end-to-end without commit.`
|
||||
- status: `done`
|
||||
- started_utc: `2026-02-22T19:47:35Z`
|
||||
- last_update_utc: `2026-02-22T21:56:30Z`
|
||||
|
||||
## Intent
|
||||
|
||||
- Load TASK-105 context from Backlog MCP and collect exact unsafe cast hotspots.
|
||||
- Write implementation plan via writing-plans skill with test-first steps and verification gates.
|
||||
- Execute plan with executing-plans skill, parallelizing independent slices where safe.
|
||||
|
||||
## Planned Files
|
||||
|
||||
- `src/main.ts`
|
||||
- `src/main/runtime/**/*.ts`
|
||||
- `src/main/**/*.test.ts`
|
||||
- `docs/plans/*.md`
|
||||
|
||||
## Files Touched
|
||||
|
||||
- `docs/subagents/INDEX.md`
|
||||
- `docs/subagents/agents/opencode-task105-unsafe-casts-20260222T194704Z-zfcm.md`
|
||||
- `docs/subagents/collaboration.md`
|
||||
- `docs/plans/2026-02-22-task-105-unsafe-runtime-casts.md`
|
||||
- `src/main.ts`
|
||||
- `src/main/runtime/app-runtime-main-deps.ts`
|
||||
- `src/main/runtime/cli-command-context-main-deps.ts`
|
||||
- `src/main/runtime/dictionary-runtime-main-deps.ts`
|
||||
- `src/main/runtime/field-grouping-overlay-main-deps.ts`
|
||||
- `src/main/runtime/jellyfin-client-info.ts`
|
||||
- `src/main/runtime/jellyfin-playback-launch.ts`
|
||||
- `src/main/runtime/mpv-client-runtime-service.ts`
|
||||
- `src/main/runtime/mpv-jellyfin-defaults.ts`
|
||||
- `src/main/runtime/mpv-main-event-bindings.ts`
|
||||
- `src/main/runtime/mpv-osd-log-main-deps.ts`
|
||||
- `src/main/runtime/overlay-runtime-bootstrap-handlers.ts`
|
||||
- `src/main/runtime/overlay-runtime-options-main-deps.ts`
|
||||
- `src/main/runtime/overlay-runtime-options.ts`
|
||||
- `src/main/runtime/overlay-visibility-runtime-main-deps.ts`
|
||||
- `src/main/runtime/startup-config-main-deps.ts`
|
||||
- `src/main/runtime/subtitle-tokenization-main-deps.ts`
|
||||
- `src/main/runtime/tray-runtime-handlers.ts`
|
||||
- `src/main/runtime/cli-command-context-factory.test.ts`
|
||||
- `src/main/runtime/composers/app-ready-composer.test.ts`
|
||||
- `src/main/runtime/composers/mpv-runtime-composer.test.ts`
|
||||
- `src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts`
|
||||
- `src/main/runtime/overlay-runtime-options-main-deps.test.ts`
|
||||
- `src/main/runtime/cli-command-context-main-deps.test.ts`
|
||||
- `src/main/runtime/subtitle-tokenization-main-deps.test.ts`
|
||||
- `src/main/runtime/jellyfin-playback-launch-main-deps.test.ts`
|
||||
- `src/main/runtime/jellyfin-playback-launch.test.ts`
|
||||
- `src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts`
|
||||
- `src/main/runtime/mpv-jellyfin-defaults.test.ts`
|
||||
- `src/main/runtime/field-grouping-overlay-main-deps.test.ts`
|
||||
- `src/main/runtime/mpv-osd-log-main-deps.test.ts`
|
||||
- `src/main/runtime/overlay-visibility-runtime-main-deps.test.ts`
|
||||
- `src/main/runtime/startup-config-main-deps.test.ts`
|
||||
- `backlog/tasks/task-105 - Eliminate-unsafe-non-test-runtime-casts-in-main-boundaries.md`
|
||||
|
||||
## Assumptions
|
||||
|
||||
- TASK-105 scope excludes test-only casts and non-main-runtime paths.
|
||||
- Existing compile/test lanes are authoritative for regression detection.
|
||||
|
||||
## Phase Log
|
||||
|
||||
- `2026-02-22T19:47:35Z` Session started; loaded backlog workflow overview, subagent coordination docs, and TASK-105 details.
|
||||
- `2026-02-22T19:52:30Z` Wrote plan at `docs/plans/2026-02-22-task-105-unsafe-runtime-casts.md`, set TASK-105 In Progress, and executed runtime cast-elimination with parallel subagents.
|
||||
- `2026-02-22T21:10:04Z` Completed cast elimination in non-test target scope; follow-up type harmonization done for tray/bootstrap/test contracts.
|
||||
- `2026-02-22T21:55:38Z` Verification complete: cast scan after=0, targeted runtime tests green, `test:core:src` green; temporarily blocked on unrelated `note-update-workflow.test.ts` TS errors during `bun run build`.
|
||||
- `2026-02-22T21:56:30Z` Re-ran `bun run build` after concurrent compile-fix; build passes. TASK-105 finalized Done in Backlog with AC/DoD evidence.
|
||||
|
||||
## Next Step
|
||||
|
||||
- TASK-105 complete. Optional next step: stage/commit focused TASK-105 diff once user requests commit.
|
||||
@@ -103,6 +103,7 @@ Shared notes. Append-only.
|
||||
- [2026-02-22T07:45:58Z] [codex-overlay-toggle-regression-20260222T073450Z-q7m4|codex-overlay-toggle-regression] added explicit overlay BrowserWindow sandbox guard (`webPreferences.sandbox=false`) to avoid preload API break on newer Electron defaults; added regression test `src/core/services/overlay-window-config.test.ts`; verified focused tests + build green.
|
||||
|
||||
## 2026-02-22
|
||||
|
||||
- [2026-02-22T08:04:10Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] starting feature: port intro skip to AniSkip API in `plugin/subminer.lua` with chapter markers + in-range OSD skip button; scoped to mpv plugin/docs only.
|
||||
- [2026-02-22T08:05:38Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] implemented AniSkip OP lookup + chapter markers + in-range OSD skip prompt/key in `plugin/subminer.lua`; updated plugin conf/docs; syntax check `luac -p plugin/subminer.lua` passed.
|
||||
- [2026-02-22T08:13:40Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] follow-up applied: launcher now runs `guessit` for file playback and passes AniSkip title/season/episode script-opts (fallback title from filename), and intro hint now displays for first 3s from intro start (`You can skip by pressing y-k`).
|
||||
@@ -116,3 +117,20 @@ Shared notes. Append-only.
|
||||
- [2026-02-22T10:04:20Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] launcher metadata fix: prefer guessit `series` for AniSkip title and fallback to show-directory extraction (`.../<Show>/Season-*`) instead of episode filename title.
|
||||
- [2026-02-22T10:10:30Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] plugin hardening: path-derived show title now prioritized over script-opt title for AniSkip lookups, reducing dependence on launcher metadata correctness.
|
||||
- [2026-02-22T10:14:40Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] simplified MAL resolution policy to first-result selection (no score/reject) per user preference.
|
||||
- [2026-02-22T19:41:00Z] [codex-discord-presence-task-20260222T194048Z-d7k2|codex-discord-presence-task] created `TASK-109` for optional Discord Rich Presence integration with polished activity card UI; no code/runtime changes in this pass.
|
||||
- [2026-02-22T19:46:00Z] [codex-task108-aniskip-20260222T194600Z-qgdt|codex-task108-aniskip] starting TASK-108 closure pass via Backlog MCP + writing-plans/executing-plans; validating plugin/docs behavior and finalizing AC/DoD evidence.
|
||||
- [2026-02-22T19:49:30Z] [codex-task108-aniskip-20260222T194600Z-qgdt|codex-task108-aniskip] completed TASK-108 closure: wrote plan artifact, revalidated plugin/docs AC coverage, ran validation suite (`luac` + launcher tests pass), finalized backlog task as Done; `tsc --noEmit` still fails on unrelated pre-existing `src/anki-integration/note-update-workflow.test.ts` errors.
|
||||
- [2026-02-22T19:47:35Z] [opencode-task105-unsafe-casts-20260222T194704Z-zfcm|opencode-task105-unsafe-casts] starting TASK-105 via Backlog MCP + writing-plans/executing-plans; scope runtime cast elimination in `src/main.ts` + `src/main/runtime/*`, no commit.
|
||||
- [2026-02-22T19:47:08Z] [codex-task104-launcher-config-20260222T194708Z-z9x1|codex-task104-launcher-config] starting TASK-104 via Backlog MCP + writing-plans/executing-plans; scope launcher config module extraction + focused tests + launcher lanes validation.
|
||||
- [2026-02-22T19:56:26Z] [codex-task104-launcher-config-20260222T194708Z-z9x1|codex-task104-launcher-config] completed TASK-104: split `launcher/config.ts` into domain modules + CLI builder/normalizer, added focused parser tests, aligned Jellyfin config reader contract (no token/userId fields), and verified `test:launcher` + `test:fast`; backlog task marked Done.
|
||||
- [2026-02-22T19:51:09Z] [opencode-task106-immersion-modules-20260222T195109Z-r3m7|opencode-task106-immersion-modules] starting TASK-106 via Backlog MCP + writing-plans/executing-plans; scope immersion tracker storage/session/metadata decomposition + focused tests + architecture docs update, no commit.
|
||||
- [2026-02-22T19:54:23Z] [codex-task105-sliceb-20260222T195423Z-w8n3|codex-task105-sliceb] overlap note: executing user-requested TASK-105 slice B in targeted runtime cast-removal files (`src/main/runtime/*-main-deps.ts`, jellyfin/mpv runtime helpers) while preserving existing TASK-105 planning artifacts.
|
||||
- [2026-02-22T19:59:42Z] [codex-task105-sliceb-20260222T195423Z-w8n3|codex-task105-sliceb] completed requested slice B: removed unsafe casts in targeted runtime modules, tightened contracts with shared runtime types (`CliCommandContextFactoryDeps`, `TokenizerDepsRuntimeOptions`, overlay options deps), and verified via focused runtime test suite (16 pass, 0 fail).
|
||||
- [2026-02-22T20:02:06Z] [codex-task105-sliceb-20260222T195423Z-w8n3|codex-task105-sliceb] follow-up typing fallout pass: updated focused runtime tests for stricter contract shapes (texthooker/anilist/mpv/jellyfin/tokenizer stubs) and revalidated same 8-file suite green.
|
||||
- [2026-02-22T20:01:45Z] [opencode-task106-immersion-modules-20260222T195109Z-r3m7|opencode-task106-immersion-modules] completed TASK-106 implementation scope: extracted `storage.ts`, `session.ts`, `metadata.ts`; reduced `immersion-tracker-service.ts` to 654 LOC facade; added focused tests (`storage-session.test.ts`, `metadata.test.ts`); tracker/core source tests green. Task finalization blocked on AC#4 build gate due unrelated pre-existing TS errors in `src/anki-integration/*` and `src/main/runtime/*`.
|
||||
- [2026-02-22T21:58:45Z] [opencode-task106-immersion-modules-20260222T195109Z-r3m7|opencode-task106-immersion-modules] blocker cleared; reran `bun run build` + tracker tests + `test:core:src` green; finalized TASK-106 to Done in Backlog with final summary.
|
||||
|
||||
- [2026-02-22T21:54:29Z] [codex-ts-build-errors-20260222T215411Z-h3k7|codex-ts-build-errors] overlap note: touching `src/main/runtime/*` and `src/anki-integration/note-update-workflow.test.ts` for strict-typing fallout fixes after TASK-105 slice-B contract tightening; scope limited to compile-error remediation + test stub alignment.
|
||||
|
||||
- [2026-02-22T21:55:54Z] [codex-ts-build-errors-20260222T215411Z-h3k7|codex-ts-build-errors] completed compile-fix pass: widened `note-update-workflow.test.ts` harness deps to `NoteUpdateWorkflowDeps`, aligned stub callback signatures, and verified `bun run tsc --noEmit` + `make build` green.
|
||||
- [2026-02-22T21:56:30Z] [opencode-task105-unsafe-casts-20260222T194704Z-zfcm|opencode-task105-unsafe-casts] finalized TASK-105: re-ran `bun run build` after compile-fix pass (green), confirmed cast scan 42->0 in scope, and moved backlog task to Done with AC/DoD + final summary.
|
||||
|
||||
93
src/main.ts
93
src/main.ts
@@ -514,7 +514,7 @@ let yomitanLoadInFlight: Promise<Extension | null> | null = null;
|
||||
|
||||
const buildApplyJellyfinMpvDefaultsMainDepsHandler =
|
||||
createBuildApplyJellyfinMpvDefaultsMainDepsHandler({
|
||||
sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client as never, command),
|
||||
sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client, command),
|
||||
jellyfinLangPref: JELLYFIN_LANG_PREF,
|
||||
});
|
||||
const applyJellyfinMpvDefaultsMainDeps = buildApplyJellyfinMpvDefaultsMainDepsHandler();
|
||||
@@ -522,7 +522,9 @@ const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler(
|
||||
applyJellyfinMpvDefaultsMainDeps,
|
||||
);
|
||||
|
||||
function applyJellyfinMpvDefaults(client: MpvIpcClient): void {
|
||||
function applyJellyfinMpvDefaults(
|
||||
client: Parameters<typeof applyJellyfinMpvDefaultsHandler>[0],
|
||||
): void {
|
||||
applyJellyfinMpvDefaultsHandler(client);
|
||||
}
|
||||
|
||||
@@ -718,36 +720,35 @@ const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsH
|
||||
let appTray: Tray | null = null;
|
||||
const buildSubtitleProcessingControllerMainDepsHandler =
|
||||
createBuildSubtitleProcessingControllerMainDepsHandler({
|
||||
tokenizeSubtitle: async (text: string) => {
|
||||
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
|
||||
return null;
|
||||
}
|
||||
return await tokenizeSubtitle(text);
|
||||
},
|
||||
emitSubtitle: (payload) => {
|
||||
const previousSubtitleText = appState.currentSubtitleData?.text ?? null;
|
||||
const nextSubtitleText = payload?.text ?? null;
|
||||
const subtitleChanged = previousSubtitleText !== nextSubtitleText;
|
||||
appState.currentSubtitleData = payload;
|
||||
if (subtitleChanged) {
|
||||
appState.hoveredSubtitleTokenIndex = null;
|
||||
appState.hoveredSubtitleRevision += 1;
|
||||
applyHoveredTokenOverlay();
|
||||
}
|
||||
broadcastToOverlayWindows('subtitle:set', payload);
|
||||
subtitleWsService.broadcast(payload, {
|
||||
enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX,
|
||||
mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode,
|
||||
});
|
||||
},
|
||||
logDebug: (message) => {
|
||||
logger.debug(`[subtitle-processing] ${message}`);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
});
|
||||
const subtitleProcessingControllerMainDeps =
|
||||
buildSubtitleProcessingControllerMainDepsHandler();
|
||||
tokenizeSubtitle: async (text: string) => {
|
||||
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
|
||||
return null;
|
||||
}
|
||||
return await tokenizeSubtitle(text);
|
||||
},
|
||||
emitSubtitle: (payload) => {
|
||||
const previousSubtitleText = appState.currentSubtitleData?.text ?? null;
|
||||
const nextSubtitleText = payload?.text ?? null;
|
||||
const subtitleChanged = previousSubtitleText !== nextSubtitleText;
|
||||
appState.currentSubtitleData = payload;
|
||||
if (subtitleChanged) {
|
||||
appState.hoveredSubtitleTokenIndex = null;
|
||||
appState.hoveredSubtitleRevision += 1;
|
||||
applyHoveredTokenOverlay();
|
||||
}
|
||||
broadcastToOverlayWindows('subtitle:set', payload);
|
||||
subtitleWsService.broadcast(payload, {
|
||||
enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX,
|
||||
mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode,
|
||||
});
|
||||
},
|
||||
logDebug: (message) => {
|
||||
logger.debug(`[subtitle-processing] ${message}`);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
});
|
||||
const subtitleProcessingControllerMainDeps = buildSubtitleProcessingControllerMainDepsHandler();
|
||||
const subtitleProcessingController = createSubtitleProcessingController(
|
||||
subtitleProcessingControllerMainDeps,
|
||||
);
|
||||
@@ -811,20 +812,20 @@ const watchConfigPathHandler = createWatchConfigPathHandler(buildWatchConfigPath
|
||||
const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler(
|
||||
{
|
||||
setKeybindings: (keybindings) => {
|
||||
appState.keybindings = keybindings as never;
|
||||
appState.keybindings = keybindings;
|
||||
},
|
||||
refreshGlobalAndOverlayShortcuts: () => {
|
||||
refreshGlobalAndOverlayShortcuts();
|
||||
},
|
||||
setSecondarySubMode: (mode) => {
|
||||
appState.secondarySubMode = mode as never;
|
||||
appState.secondarySubMode = mode;
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, payload) => {
|
||||
broadcastToOverlayWindows(channel, payload);
|
||||
},
|
||||
applyAnkiRuntimeConfigPatch: (patch) => {
|
||||
if (appState.ankiIntegration) {
|
||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch as never);
|
||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -912,7 +913,7 @@ const jlptDictionaryRuntime = createJlptDictionaryRuntimeService(
|
||||
getDictionaryRoots: () => buildDictionaryRootsHandler(),
|
||||
getJlptDictionarySearchPaths,
|
||||
setJlptLevelLookup: (lookup) => {
|
||||
appState.jlptLevelLookup = lookup as never;
|
||||
appState.jlptLevelLookup = lookup;
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
})(),
|
||||
@@ -926,7 +927,7 @@ const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService(
|
||||
getFrequencyDictionarySearchPaths,
|
||||
getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath,
|
||||
setFrequencyRankLookup: (lookup) => {
|
||||
appState.frequencyRankLookup = lookup as never;
|
||||
appState.frequencyRankLookup = lookup;
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
})(),
|
||||
@@ -968,7 +969,7 @@ function setFieldGroupingResolver(
|
||||
}
|
||||
|
||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>(
|
||||
createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal, KikuFieldGroupingChoice>({
|
||||
createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal>({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
@@ -1257,8 +1258,7 @@ const buildPlayJellyfinItemInMpvMainDepsHandler = createBuildPlayJellyfinItemInM
|
||||
subtitleStreamIndex: params.subtitleStreamIndex ?? undefined,
|
||||
},
|
||||
),
|
||||
applyJellyfinMpvDefaults: (mpvClient) =>
|
||||
applyJellyfinMpvDefaults(mpvClient as unknown as MpvIpcClient),
|
||||
applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults(mpvClient),
|
||||
sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command),
|
||||
armQuitOnDisconnect: () => {
|
||||
jellyfinPlayQuitOnDisconnectArmed = false;
|
||||
@@ -2169,10 +2169,7 @@ const {
|
||||
},
|
||||
},
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: MpvIpcClient as unknown as new (
|
||||
socketPath: string,
|
||||
options: MpvClientRuntimeServiceOptions,
|
||||
) => MpvIpcClient,
|
||||
createClient: MpvIpcClient,
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
|
||||
@@ -2434,7 +2431,7 @@ const { appendToMpvLog, flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers(
|
||||
buildShowMpvOsdMainDeps: (appendToMpvLogHandler) => ({
|
||||
appendToMpvLog: (message) => appendToMpvLogHandler(message),
|
||||
showMpvOsdRuntime: (mpvClient, text, fallbackLog) =>
|
||||
showMpvOsdRuntime(mpvClient as never, text, fallbackLog),
|
||||
showMpvOsdRuntime(mpvClient, text, fallbackLog),
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
logInfo: (line) => logger.info(line),
|
||||
}),
|
||||
@@ -2845,7 +2842,7 @@ const {
|
||||
},
|
||||
createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
|
||||
createEmptyImage: () => nativeImage.createEmpty(),
|
||||
createTray: (icon) => new Tray(icon as never),
|
||||
createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
|
||||
trayTooltip: TRAY_TOOLTIP,
|
||||
platform: process.platform,
|
||||
logWarn: (message) => logger.warn(message),
|
||||
@@ -2910,12 +2907,12 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
||||
getOverlayWindows: () => getOverlayWindows(),
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
showDesktopNotification,
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback() as never,
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||
},
|
||||
initializeOverlayRuntimeBootstrapDeps: {
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlayRuntimeCore: (options) => initializeOverlayRuntimeCore(options as never),
|
||||
initializeOverlayRuntimeCore,
|
||||
setInvisibleOverlayVisible: (visible) => {
|
||||
overlayManager.setInvisibleOverlayVisible(visible);
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export function createBuildEnsureTrayMainDepsHandler(deps: {
|
||||
getTray: () => unknown | null;
|
||||
setTray: (tray: unknown | null) => void;
|
||||
buildTrayMenu: () => unknown;
|
||||
export function createBuildEnsureTrayMainDepsHandler<TTray, TTrayMenu, TTrayIcon>(deps: {
|
||||
getTray: () => TTray | null;
|
||||
setTray: (tray: TTray | null) => void;
|
||||
buildTrayMenu: () => TTrayMenu;
|
||||
resolveTrayIconPath: () => string | null;
|
||||
createImageFromPath: (iconPath: string) => unknown;
|
||||
createEmptyImage: () => unknown;
|
||||
createTray: (icon: unknown) => unknown;
|
||||
createImageFromPath: (iconPath: string) => TTrayIcon;
|
||||
createEmptyImage: () => TTrayIcon;
|
||||
createTray: (icon: TTrayIcon) => TTray;
|
||||
trayTooltip: string;
|
||||
platform: string;
|
||||
logWarn: (message: string) => void;
|
||||
@@ -14,13 +14,13 @@ export function createBuildEnsureTrayMainDepsHandler(deps: {
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getTray: () => deps.getTray() as never,
|
||||
setTray: (tray: unknown | null) => deps.setTray(tray),
|
||||
buildTrayMenu: () => deps.buildTrayMenu() as never,
|
||||
getTray: () => deps.getTray(),
|
||||
setTray: (tray: TTray | null) => deps.setTray(tray),
|
||||
buildTrayMenu: () => deps.buildTrayMenu(),
|
||||
resolveTrayIconPath: () => deps.resolveTrayIconPath(),
|
||||
createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath) as never,
|
||||
createEmptyImage: () => deps.createEmptyImage() as never,
|
||||
createTray: (icon: unknown) => deps.createTray(icon) as never,
|
||||
createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath),
|
||||
createEmptyImage: () => deps.createEmptyImage(),
|
||||
createTray: (icon: TTrayIcon) => deps.createTray(icon),
|
||||
trayTooltip: deps.trayTooltip,
|
||||
platform: deps.platform,
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
@@ -33,28 +33,28 @@ export function createBuildEnsureTrayMainDepsHandler(deps: {
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildDestroyTrayMainDepsHandler(deps: {
|
||||
getTray: () => unknown | null;
|
||||
setTray: (tray: unknown | null) => void;
|
||||
export function createBuildDestroyTrayMainDepsHandler<TTray>(deps: {
|
||||
getTray: () => TTray | null;
|
||||
setTray: (tray: TTray | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getTray: () => deps.getTray() as never,
|
||||
setTray: (tray: unknown | null) => deps.setTray(tray),
|
||||
getTray: () => deps.getTray(),
|
||||
setTray: (tray: TTray | null) => deps.setTray(tray),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps: {
|
||||
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOptions>(deps: {
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
initializeOverlayRuntimeCore: (options: unknown) => { invisibleOverlayVisible: boolean };
|
||||
buildOptions: () => unknown;
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => { invisibleOverlayVisible: boolean };
|
||||
buildOptions: () => TOptions;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) => void;
|
||||
startBackgroundWarmups: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
initializeOverlayRuntimeCore: (options: unknown) => deps.initializeOverlayRuntimeCore(options),
|
||||
buildOptions: () => deps.buildOptions() as never,
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options),
|
||||
buildOptions: () => deps.buildOptions(),
|
||||
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) =>
|
||||
deps.setOverlayRuntimeInitialized(initialized),
|
||||
@@ -62,27 +62,27 @@ export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildOpenYomitanSettingsMainDepsHandler(deps: {
|
||||
ensureYomitanExtensionLoaded: () => Promise<unknown | null>;
|
||||
export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWindow>(deps: {
|
||||
ensureYomitanExtensionLoaded: () => Promise<TYomitanExt | null>;
|
||||
openYomitanSettingsWindow: (params: {
|
||||
yomitanExt: unknown;
|
||||
getExistingWindow: () => unknown | null;
|
||||
setWindow: (window: unknown | null) => void;
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
}) => void;
|
||||
getExistingWindow: () => unknown | null;
|
||||
setWindow: (window: unknown | null) => void;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, error: unknown) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
|
||||
openYomitanSettingsWindow: (params: {
|
||||
yomitanExt: unknown;
|
||||
getExistingWindow: () => unknown | null;
|
||||
setWindow: (window: unknown | null) => void;
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
}) => deps.openYomitanSettingsWindow(params),
|
||||
getExistingWindow: () => deps.getExistingWindow(),
|
||||
setWindow: (window: unknown | null) => deps.setWindow(window),
|
||||
setWindow: (window: TWindow | null) => deps.setWindow(window),
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
logError: (message: string, error: unknown) => deps.logError(message, error),
|
||||
});
|
||||
|
||||
@@ -6,14 +6,14 @@ test('cli command context factory composes main deps and context handlers', () =
|
||||
const calls: string[] = [];
|
||||
const appState = {
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
mpvClient: null as unknown,
|
||||
mpvClient: null,
|
||||
texthookerPort: 5174,
|
||||
overlayRuntimeInitialized: false,
|
||||
};
|
||||
|
||||
const createContext = createCliCommandContextFactory({
|
||||
appState,
|
||||
texthookerService: { start: () => null },
|
||||
texthookerService: { isRunning: () => false, start: () => null },
|
||||
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }),
|
||||
openExternal: async () => {},
|
||||
logBrowserOpenError: () => {},
|
||||
@@ -32,11 +32,28 @@ test('cli command context factory composes main deps and context handlers', () =
|
||||
triggerFieldGrouping: async () => {},
|
||||
triggerSubsyncFromConfig: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
getAnilistStatus: () => ({ status: 'ok' }),
|
||||
getAnilistStatus: () => ({
|
||||
tokenStatus: 'resolved',
|
||||
tokenSource: 'literal',
|
||||
tokenMessage: null,
|
||||
tokenResolvedAt: null,
|
||||
tokenErrorAt: null,
|
||||
queuePending: 0,
|
||||
queueReady: 0,
|
||||
queueDeadLetter: 0,
|
||||
queueLastAttemptAt: null,
|
||||
queueLastError: null,
|
||||
}),
|
||||
clearAnilistToken: () => {},
|
||||
openAnilistSetupWindow: () => {},
|
||||
openJellyfinSetupWindow: () => {},
|
||||
getAnilistQueueStatus: () => ({ queued: 0 }),
|
||||
getAnilistQueueStatus: () => ({
|
||||
pending: 0,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
lastAttemptAt: null,
|
||||
lastError: null,
|
||||
}),
|
||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
||||
runJellyfinCommand: async () => {},
|
||||
openYomitanSettings: () => {},
|
||||
|
||||
@@ -6,14 +6,14 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
||||
const calls: string[] = [];
|
||||
const appState = {
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
mpvClient: null as unknown,
|
||||
mpvClient: null,
|
||||
texthookerPort: 5174,
|
||||
overlayRuntimeInitialized: false,
|
||||
};
|
||||
|
||||
const build = createBuildCliCommandContextMainDepsHandler({
|
||||
appState,
|
||||
texthookerService: { start: () => null },
|
||||
texthookerService: { isRunning: () => false, start: () => null },
|
||||
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }),
|
||||
openExternal: async (url) => {
|
||||
calls.push(`open:${url}`);
|
||||
@@ -49,11 +49,28 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
||||
calls.push('mark-audio');
|
||||
},
|
||||
|
||||
getAnilistStatus: () => ({ status: 'ok' }),
|
||||
getAnilistStatus: () => ({
|
||||
tokenStatus: 'resolved',
|
||||
tokenSource: 'literal',
|
||||
tokenMessage: null,
|
||||
tokenResolvedAt: null,
|
||||
tokenErrorAt: null,
|
||||
queuePending: 0,
|
||||
queueReady: 0,
|
||||
queueDeadLetter: 0,
|
||||
queueLastAttemptAt: null,
|
||||
queueLastError: null,
|
||||
}),
|
||||
clearAnilistToken: () => calls.push('clear-token'),
|
||||
openAnilistSetupWindow: () => calls.push('open-anilist-setup'),
|
||||
openJellyfinSetupWindow: () => calls.push('open-jellyfin-setup'),
|
||||
getAnilistQueueStatus: () => ({ queued: 1 }),
|
||||
getAnilistQueueStatus: () => ({
|
||||
pending: 1,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
lastAttemptAt: null,
|
||||
lastError: null,
|
||||
}),
|
||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
||||
runJellyfinCommand: async () => {
|
||||
calls.push('run-jellyfin');
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { CliArgs } from '../../cli/args';
|
||||
import type { CliCommandContextFactoryDeps } from './cli-command-context';
|
||||
|
||||
type CliCommandContextMainState = {
|
||||
mpvSocketPath: string;
|
||||
mpvClient: ReturnType<CliCommandContextFactoryDeps['getMpvClient']>;
|
||||
texthookerPort: number;
|
||||
overlayRuntimeInitialized: boolean;
|
||||
};
|
||||
|
||||
export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
appState: {
|
||||
mpvSocketPath: string;
|
||||
mpvClient: unknown | null;
|
||||
texthookerPort: number;
|
||||
overlayRuntimeInitialized: boolean;
|
||||
};
|
||||
texthookerService: unknown;
|
||||
appState: CliCommandContextMainState;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getResolvedConfig: () => { texthooker?: { openBrowser?: boolean } };
|
||||
openExternal: (url: string) => Promise<unknown>;
|
||||
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||
@@ -29,12 +32,12 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
|
||||
getAnilistStatus: () => unknown;
|
||||
getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
|
||||
clearAnilistToken: () => void;
|
||||
openAnilistSetupWindow: () => void;
|
||||
openJellyfinSetupWindow: () => void;
|
||||
getAnilistQueueStatus: () => unknown;
|
||||
processNextAnilistRetryUpdate: () => Promise<{ ok: boolean; message: string }>;
|
||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||
processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
|
||||
openYomitanSettings: () => void;
|
||||
@@ -49,14 +52,14 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
return (): CliCommandContextFactoryDeps => ({
|
||||
getSocketPath: () => deps.appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
deps.appState.mpvSocketPath = socketPath;
|
||||
},
|
||||
getMpvClient: () => deps.appState.mpvClient as never,
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
showOsd: (text: string) => deps.showMpvOsd(text),
|
||||
texthookerService: deps.texthookerService as never,
|
||||
texthookerService: deps.texthookerService,
|
||||
getTexthookerPort: () => deps.appState.texthookerPort,
|
||||
setTexthookerPort: (port: number) => {
|
||||
deps.appState.texthookerPort = port;
|
||||
@@ -80,11 +83,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
triggerFieldGrouping: () => deps.triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => deps.markLastCardAsAudioCard(),
|
||||
getAnilistStatus: () => deps.getAnilistStatus() as never,
|
||||
getAnilistStatus: () => deps.getAnilistStatus(),
|
||||
clearAnilistToken: () => deps.clearAnilistToken(),
|
||||
openAnilistSetup: () => deps.openAnilistSetupWindow(),
|
||||
openJellyfinSetup: () => deps.openJellyfinSetupWindow(),
|
||||
getAnilistQueueStatus: () => deps.getAnilistQueueStatus() as never,
|
||||
getAnilistQueueStatus: () => deps.getAnilistQueueStatus(),
|
||||
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
|
||||
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
|
||||
openYomitanSettings: () => deps.openYomitanSettings(),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { composeAppReadyRuntime } from './app-ready-composer';
|
||||
test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => {
|
||||
const composed = composeAppReadyRuntime({
|
||||
reloadConfigMainDeps: {
|
||||
reloadConfigStrict: () => ({ config: {} as never, warnings: [] }),
|
||||
reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
|
||||
logInfo: () => {},
|
||||
logWarning: () => {},
|
||||
showDesktopNotification: () => {},
|
||||
|
||||
@@ -85,7 +85,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: FakeMpvClient,
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
||||
isAutoStartOverlayEnabled: () => true,
|
||||
setOverlayVisible: () => {},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
@@ -118,7 +118,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
setYomitanParserInitPromise: () => {},
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: () => {},
|
||||
getKnownWordMatchMode: () => 'exact',
|
||||
getKnownWordMatchMode: () => 'headword',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => null,
|
||||
getJlptEnabled: () => true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main-
|
||||
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps';
|
||||
import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service';
|
||||
import type { MpvClientRuntimeServiceOptions } from '../mpv-client-runtime-service';
|
||||
import type { Config } from '../../../types';
|
||||
import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from '../mpv-subtitle-render-metrics-main-deps';
|
||||
import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics';
|
||||
import {
|
||||
@@ -30,7 +31,7 @@ type MpvClientRuntimeServiceFactoryMainDeps<TMpvClient extends RuntimeMpvClient>
|
||||
Parameters<
|
||||
typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
unknown,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>
|
||||
>[0],
|
||||
@@ -107,7 +108,7 @@ export function composeMpvRuntimeHandlers<
|
||||
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
||||
createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
unknown,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>({
|
||||
...options.mpvClientRuntimeServiceFactoryMainDeps,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type { FrequencyDictionaryLookup, JlptLevel } from '../../types';
|
||||
|
||||
type JlptLookup = (term: string) => JlptLevel | null;
|
||||
|
||||
export function createBuildDictionaryRootsMainHandler(deps: {
|
||||
dirname: string;
|
||||
appPath: string;
|
||||
@@ -8,20 +12,19 @@ export function createBuildDictionaryRootsMainHandler(deps: {
|
||||
cwd: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
}) {
|
||||
return () =>
|
||||
[
|
||||
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.userDataPath,
|
||||
deps.appUserDataPath,
|
||||
deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, '.config', 'subminer'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
|
||||
deps.cwd,
|
||||
];
|
||||
return () => [
|
||||
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.userDataPath,
|
||||
deps.appUserDataPath,
|
||||
deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, '.config', 'subminer'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
|
||||
deps.cwd,
|
||||
];
|
||||
}
|
||||
|
||||
export function createBuildFrequencyDictionaryRootsMainHandler(deps: {
|
||||
@@ -57,7 +60,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
|
||||
isJlptEnabled: () => boolean;
|
||||
getDictionaryRoots: () => string[];
|
||||
getJlptDictionarySearchPaths: (deps: { getDictionaryRoots: () => string[] }) => string[];
|
||||
setJlptLevelLookup: (lookup: unknown) => void;
|
||||
setJlptLevelLookup: (lookup: JlptLookup) => void;
|
||||
logInfo: (message: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
@@ -66,7 +69,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
|
||||
deps.getJlptDictionarySearchPaths({
|
||||
getDictionaryRoots: () => deps.getDictionaryRoots(),
|
||||
}),
|
||||
setJlptLevelLookup: (lookup: unknown) => deps.setJlptLevelLookup(lookup),
|
||||
setJlptLevelLookup: (lookup: JlptLookup) => deps.setJlptLevelLookup(lookup),
|
||||
log: (message: string) => deps.logInfo(`[JLPT] ${message}`),
|
||||
});
|
||||
}
|
||||
@@ -79,17 +82,19 @@ export function createBuildFrequencyDictionaryRuntimeMainDepsHandler(deps: {
|
||||
getSourcePath: () => string | undefined;
|
||||
}) => string[];
|
||||
getSourcePath: () => string | undefined;
|
||||
setFrequencyRankLookup: (lookup: unknown) => void;
|
||||
setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) => void;
|
||||
logInfo: (message: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isFrequencyDictionaryEnabled: () => deps.isFrequencyDictionaryEnabled(),
|
||||
getSearchPaths: () =>
|
||||
deps.getFrequencyDictionarySearchPaths({
|
||||
getDictionaryRoots: () => deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot),
|
||||
getDictionaryRoots: () =>
|
||||
deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot),
|
||||
getSourcePath: () => deps.getSourcePath(),
|
||||
}),
|
||||
setFrequencyRankLookup: (lookup: unknown) => deps.setFrequencyRankLookup(lookup),
|
||||
setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) =>
|
||||
deps.setFrequencyRankLookup(lookup),
|
||||
log: (message: string) => deps.logInfo(`[Frequency] ${message}`),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ test('field grouping overlay main deps builder maps window visibility and resolv
|
||||
const resolver = (choice: unknown) => calls.push(`resolver:${choice}`);
|
||||
|
||||
const deps = createBuildFieldGroupingOverlayMainDepsHandler({
|
||||
getMainWindow: () => ({ id: 'main' }),
|
||||
getMainWindow: () => ({
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
send: () => {},
|
||||
},
|
||||
}),
|
||||
getVisibleOverlayVisible: () => true,
|
||||
getInvisibleOverlayVisible: () => false,
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
|
||||
@@ -24,7 +29,7 @@ test('field grouping overlay main deps builder maps window visibility and resolv
|
||||
},
|
||||
})();
|
||||
|
||||
assert.deepEqual(deps.getMainWindow(), { id: 'main' });
|
||||
assert.equal(deps.getMainWindow()?.isDestroyed(), false);
|
||||
assert.equal(deps.getVisibleOverlayVisible(), true);
|
||||
assert.equal(deps.getInvisibleOverlayVisible(), false);
|
||||
assert.equal(deps.getResolver(), resolver);
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
export function createBuildFieldGroupingOverlayMainDepsHandler<
|
||||
TModal extends string,
|
||||
TChoice,
|
||||
>(deps: {
|
||||
getMainWindow: () => unknown | null;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
getResolver: () => ((choice: TChoice) => void) | null;
|
||||
setResolver: (resolver: ((choice: TChoice) => void) | null) => void;
|
||||
getRestoreVisibleOverlayOnModalClose: () => Set<TModal>;
|
||||
import type { FieldGroupingOverlayRuntimeOptions } from '../../core/services/field-grouping-overlay';
|
||||
|
||||
type FieldGroupingOverlayMainDeps<TModal extends string> = Omit<
|
||||
FieldGroupingOverlayRuntimeOptions<TModal>,
|
||||
'sendToVisibleOverlay'
|
||||
> & {
|
||||
sendToActiveOverlayWindow: (
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: TModal },
|
||||
) => boolean;
|
||||
}) {
|
||||
return () => ({
|
||||
getMainWindow: () => deps.getMainWindow() as never,
|
||||
};
|
||||
|
||||
type BuiltFieldGroupingOverlayMainDeps<TModal extends string> =
|
||||
FieldGroupingOverlayRuntimeOptions<TModal> & {
|
||||
sendToVisibleOverlay: NonNullable<
|
||||
FieldGroupingOverlayRuntimeOptions<TModal>['sendToVisibleOverlay']
|
||||
>;
|
||||
};
|
||||
|
||||
export function createBuildFieldGroupingOverlayMainDepsHandler<TModal extends string>(
|
||||
deps: FieldGroupingOverlayMainDeps<TModal>,
|
||||
) {
|
||||
return (): BuiltFieldGroupingOverlayMainDeps<TModal> => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||
getResolver: () => deps.getResolver() as never,
|
||||
setResolver: (resolver: ((choice: TChoice) => void) | null) => deps.setResolver(resolver),
|
||||
getResolver: () => deps.getResolver(),
|
||||
setResolver: (resolver) => deps.setResolver(resolver),
|
||||
getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(),
|
||||
sendToVisibleOverlay: (
|
||||
channel: string,
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import type { JellyfinStoredSession } from '../../core/services/jellyfin-token-store';
|
||||
import type { ResolvedConfig } from '../../types';
|
||||
|
||||
type ResolvedJellyfinConfig = ResolvedConfig['jellyfin'];
|
||||
type ResolvedJellyfinConfigWithSession = ResolvedJellyfinConfig & {
|
||||
accessToken?: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||
getResolvedConfig: () => { jellyfin: unknown };
|
||||
loadStoredSession: () => { accessToken: string; userId: string } | null | undefined;
|
||||
getResolvedConfig: () => { jellyfin: ResolvedJellyfinConfig };
|
||||
loadStoredSession: () => JellyfinStoredSession | null | undefined;
|
||||
getEnv: (name: string) => string | undefined;
|
||||
}) {
|
||||
return () => {
|
||||
const jellyfin = deps.getResolvedConfig().jellyfin as {
|
||||
userId?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
return (): ResolvedJellyfinConfigWithSession => {
|
||||
const jellyfin = deps.getResolvedConfig().jellyfin;
|
||||
|
||||
const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? '';
|
||||
const envUserId = deps.getEnv('SUBMINER_JELLYFIN_USER_ID')?.trim() ?? '';
|
||||
@@ -20,7 +26,7 @@ export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||
...jellyfin,
|
||||
accessToken: envToken,
|
||||
userId: envUserId || storedUserId || '',
|
||||
} as never;
|
||||
};
|
||||
}
|
||||
|
||||
if (storedToken.length > 0 && storedUserId.length > 0) {
|
||||
@@ -28,24 +34,20 @@ export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||
...jellyfin,
|
||||
accessToken: storedToken,
|
||||
userId: storedUserId,
|
||||
} as never;
|
||||
};
|
||||
}
|
||||
|
||||
return jellyfin as never;
|
||||
return jellyfin;
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetJellyfinClientInfoHandler(deps: {
|
||||
getResolvedJellyfinConfig: () => {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
getDefaultJellyfinConfig: () => {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
getResolvedJellyfinConfig: () => Partial<
|
||||
Pick<ResolvedJellyfinConfig, 'clientName' | 'clientVersion' | 'deviceId'>
|
||||
>;
|
||||
getDefaultJellyfinConfig: () => Partial<
|
||||
Pick<ResolvedJellyfinConfig, 'clientName' | 'clientVersion' | 'deviceId'>
|
||||
>;
|
||||
}) {
|
||||
return (
|
||||
config = deps.getResolvedJellyfinConfig(),
|
||||
|
||||
@@ -6,12 +6,14 @@ test('play jellyfin item in mpv main deps builder maps callbacks', async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildPlayJellyfinItemInMpvMainDepsHandler({
|
||||
ensureMpvConnectedForPlayback: async () => true,
|
||||
getMpvClient: () => ({ connected: true }),
|
||||
getMpvClient: () => ({ connected: true, send: () => {} }),
|
||||
resolvePlaybackPlan: async () => ({
|
||||
url: 'u',
|
||||
mode: 'direct',
|
||||
title: 't',
|
||||
startTimeTicks: 0,
|
||||
audioStreamIndex: null,
|
||||
subtitleStreamIndex: null,
|
||||
}),
|
||||
applyJellyfinMpvDefaults: () => calls.push('defaults'),
|
||||
sendMpvCommand: (command) => calls.push(`cmd:${command[0]}`),
|
||||
@@ -28,18 +30,57 @@ test('play jellyfin item in mpv main deps builder maps callbacks', async () => {
|
||||
assert.equal(await deps.ensureMpvConnectedForPlayback(), true);
|
||||
assert.equal(typeof deps.getMpvClient(), 'object');
|
||||
assert.deepEqual(
|
||||
await deps.resolvePlaybackPlan({ session: {} as never, clientInfo: {} as never, jellyfinConfig: {}, itemId: 'i' }),
|
||||
{ url: 'u', mode: 'direct', title: 't', startTimeTicks: 0 },
|
||||
await deps.resolvePlaybackPlan({
|
||||
session: {
|
||||
serverUrl: 'http://localhost:8096',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
username: 'alice',
|
||||
},
|
||||
clientInfo: {
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0.0',
|
||||
deviceId: 'did',
|
||||
},
|
||||
jellyfinConfig: {},
|
||||
itemId: 'i',
|
||||
}),
|
||||
{
|
||||
url: 'u',
|
||||
mode: 'direct',
|
||||
title: 't',
|
||||
startTimeTicks: 0,
|
||||
audioStreamIndex: null,
|
||||
subtitleStreamIndex: null,
|
||||
},
|
||||
);
|
||||
deps.applyJellyfinMpvDefaults({});
|
||||
deps.applyJellyfinMpvDefaults({ connected: true, send: () => {} });
|
||||
deps.sendMpvCommand(['show-text', 'x']);
|
||||
deps.armQuitOnDisconnect();
|
||||
deps.schedule(() => {}, 500);
|
||||
assert.equal(deps.convertTicksToSeconds(20_000_000), 2);
|
||||
deps.preloadExternalSubtitles({ session: {} as never, clientInfo: {} as never, itemId: 'i' });
|
||||
deps.preloadExternalSubtitles({
|
||||
session: {
|
||||
serverUrl: 'http://localhost:8096',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
username: 'alice',
|
||||
},
|
||||
clientInfo: {
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0.0',
|
||||
deviceId: 'did',
|
||||
},
|
||||
itemId: 'i',
|
||||
});
|
||||
deps.setActivePlayback({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay' });
|
||||
deps.setLastProgressAtMs(0);
|
||||
deps.reportPlaying({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay', eventName: 'start' });
|
||||
deps.reportPlaying({
|
||||
itemId: 'i',
|
||||
mediaSourceId: undefined,
|
||||
playMethod: 'DirectPlay',
|
||||
eventName: 'start',
|
||||
});
|
||||
deps.showMpvOsd('ok');
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
|
||||
@@ -54,7 +54,7 @@ test('playback handler drives mpv commands and playback state', async () => {
|
||||
const reportPayloads: Array<Record<string, unknown>> = [];
|
||||
const handler = createPlayJellyfinItemInMpvHandler({
|
||||
ensureMpvConnectedForPlayback: async () => true,
|
||||
getMpvClient: () => ({ connected: true }),
|
||||
getMpvClient: () => ({ connected: true, send: () => {} }),
|
||||
resolvePlaybackPlan: async () => ({
|
||||
url: 'https://stream.example/video.m3u8',
|
||||
mode: 'direct',
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
type JellyfinSession = {
|
||||
serverUrl: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
};
|
||||
import type { JellyfinAuthSession, JellyfinPlaybackPlan } from '../../core/services/jellyfin';
|
||||
import type { JellyfinConfig } from '../../types';
|
||||
import type { MpvRuntimeClientLike } from '../../core/services/mpv';
|
||||
|
||||
type JellyfinClientInfo = {
|
||||
clientName: string;
|
||||
@@ -11,15 +8,6 @@ type JellyfinClientInfo = {
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
type JellyfinPlaybackPlan = {
|
||||
url: string;
|
||||
mode: 'direct' | 'transcode';
|
||||
title: string;
|
||||
startTimeTicks: number;
|
||||
audioStreamIndex?: number | null;
|
||||
subtitleStreamIndex?: number | null;
|
||||
};
|
||||
|
||||
type ActivePlaybackState = {
|
||||
itemId: string;
|
||||
mediaSourceId: undefined;
|
||||
@@ -28,26 +16,24 @@ type ActivePlaybackState = {
|
||||
playMethod: 'DirectPlay' | 'Transcode';
|
||||
};
|
||||
|
||||
type MpvClientLike = unknown;
|
||||
|
||||
export function createPlayJellyfinItemInMpvHandler(deps: {
|
||||
ensureMpvConnectedForPlayback: () => Promise<boolean>;
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
getMpvClient: () => MpvRuntimeClientLike | null;
|
||||
resolvePlaybackPlan: (params: {
|
||||
session: JellyfinSession;
|
||||
session: JellyfinAuthSession;
|
||||
clientInfo: JellyfinClientInfo;
|
||||
jellyfinConfig: unknown;
|
||||
jellyfinConfig: JellyfinConfig;
|
||||
itemId: string;
|
||||
audioStreamIndex?: number | null;
|
||||
subtitleStreamIndex?: number | null;
|
||||
}) => Promise<JellyfinPlaybackPlan>;
|
||||
applyJellyfinMpvDefaults: (mpvClient: MpvClientLike) => void;
|
||||
applyJellyfinMpvDefaults: (mpvClient: MpvRuntimeClientLike) => void;
|
||||
sendMpvCommand: (command: Array<string | number>) => void;
|
||||
armQuitOnDisconnect: () => void;
|
||||
schedule: (callback: () => void, delayMs: number) => void;
|
||||
convertTicksToSeconds: (ticks: number) => number;
|
||||
preloadExternalSubtitles: (params: {
|
||||
session: JellyfinSession;
|
||||
session: JellyfinAuthSession;
|
||||
clientInfo: JellyfinClientInfo;
|
||||
itemId: string;
|
||||
}) => void;
|
||||
@@ -64,9 +50,9 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
|
||||
showMpvOsd: (text: string) => void;
|
||||
}) {
|
||||
return async (params: {
|
||||
session: JellyfinSession;
|
||||
session: JellyfinAuthSession;
|
||||
clientInfo: JellyfinClientInfo;
|
||||
jellyfinConfig: unknown;
|
||||
jellyfinConfig: JellyfinConfig;
|
||||
itemId: string;
|
||||
audioStreamIndex?: number | null;
|
||||
subtitleStreamIndex?: number | null;
|
||||
@@ -96,7 +82,11 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
|
||||
if (params.setQuitOnDisconnectArm !== false) {
|
||||
deps.armQuitOnDisconnect();
|
||||
}
|
||||
deps.sendMpvCommand(['set_property', 'force-media-title', `[Jellyfin/${plan.mode}] ${plan.title}`]);
|
||||
deps.sendMpvCommand([
|
||||
'set_property',
|
||||
'force-media-title',
|
||||
`[Jellyfin/${plan.mode}] ${plan.title}`,
|
||||
]);
|
||||
deps.sendMpvCommand(['set_property', 'sid', 'no']);
|
||||
deps.schedule(() => {
|
||||
deps.sendMpvCommand(['set_property', 'sid', 'no']);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Config } from '../../types';
|
||||
|
||||
export type MpvClientRuntimeServiceOptions = {
|
||||
getResolvedConfig: () => unknown;
|
||||
getResolvedConfig: () => Config;
|
||||
autoStartOverlay: boolean;
|
||||
setOverlayVisible: (visible: boolean) => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
|
||||
@@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults main deps builder maps callbacks', () => {
|
||||
jellyfinLangPref: 'ja,jp',
|
||||
})();
|
||||
|
||||
deps.sendMpvCommandRuntime({}, ['set_property', 'aid', 'auto']);
|
||||
deps.sendMpvCommandRuntime({ connected: true, send: () => {} }, ['set_property', 'aid', 'auto']);
|
||||
assert.equal(deps.jellyfinLangPref, 'ja,jp');
|
||||
assert.deepEqual(calls, ['set_property:aid:auto']);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults sends expected property commands', () => {
|
||||
jellyfinLangPref: 'ja,jp',
|
||||
});
|
||||
|
||||
applyDefaults({});
|
||||
applyDefaults({ connected: true, send: () => {} });
|
||||
assert.deepEqual(calls, [
|
||||
'set_property:sub-auto:fuzzy',
|
||||
'set_property:aid:auto',
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
type MpvClientLike = unknown;
|
||||
import type { MpvRuntimeClientLike } from '../../core/services/mpv';
|
||||
|
||||
export function createApplyJellyfinMpvDefaultsHandler(deps: {
|
||||
sendMpvCommandRuntime: (
|
||||
client: MpvClientLike,
|
||||
command: [string, string, string],
|
||||
) => void;
|
||||
sendMpvCommandRuntime: (client: MpvRuntimeClientLike, command: [string, string, string]) => void;
|
||||
jellyfinLangPref: string;
|
||||
}) {
|
||||
return (client: MpvClientLike): void => {
|
||||
return (client: MpvRuntimeClientLike): void => {
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']);
|
||||
@@ -18,9 +15,7 @@ export function createApplyJellyfinMpvDefaultsHandler(deps: {
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetDefaultSocketPathHandler(deps: {
|
||||
platform: string;
|
||||
}) {
|
||||
export function createGetDefaultSocketPathHandler(deps: { platform: string }) {
|
||||
return (): string => {
|
||||
if (deps.platform === 'win32') {
|
||||
return '\\\\.\\pipe\\subminer-socket';
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
createHandleMpvTimePosChangeHandler,
|
||||
} from './mpv-main-event-actions';
|
||||
|
||||
type MpvEventClient = {
|
||||
on: (...args: any[]) => unknown;
|
||||
};
|
||||
type MpvEventClient = Parameters<ReturnType<typeof createBindMpvClientEventHandlers>>[0];
|
||||
|
||||
export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
reportJellyfinRemoteStopped: () => void;
|
||||
@@ -119,7 +117,8 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
updateSubtitleRenderMetrics: (patch) => deps.updateSubtitleRenderMetrics(patch),
|
||||
});
|
||||
const handleMpvSecondarySubtitleVisibility = createHandleMpvSecondarySubtitleVisibilityHandler({
|
||||
setPreviousSecondarySubVisibility: (visible) => deps.setPreviousSecondarySubVisibility(visible),
|
||||
setPreviousSecondarySubVisibility: (visible) =>
|
||||
deps.setPreviousSecondarySubVisibility(visible),
|
||||
});
|
||||
|
||||
createBindMpvClientEventHandlers({
|
||||
@@ -134,6 +133,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
onPauseChange: handleMpvPauseChange,
|
||||
onSubtitleMetricsChange: handleMpvSubtitleMetricsChange,
|
||||
onSecondarySubtitleVisibility: handleMpvSecondarySubtitleVisibility,
|
||||
})(mpvClient as never);
|
||||
})(mpvClient);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,10 @@ test('append to mpv log main deps map filesystem functions and log path', async
|
||||
|
||||
test('show mpv osd main deps map runtime delegates and logging callback', () => {
|
||||
const calls: string[] = [];
|
||||
const client = { id: 'mpv' };
|
||||
const client = {
|
||||
connected: true,
|
||||
send: () => {},
|
||||
};
|
||||
const deps = createBuildShowMpvOsdMainDepsHandler({
|
||||
appendToMpvLog: (message) => calls.push(`append:${message}`),
|
||||
showMpvOsdRuntime: (_mpvClient, text, fallbackLog) => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export function createBuildAppendToMpvLogMainDepsHandler(deps: {
|
||||
logPath: string;
|
||||
dirname: (targetPath: string) => string;
|
||||
mkdir: (targetPath: string, options: { recursive: boolean }) => Promise<void>;
|
||||
appendFile: (targetPath: string, data: string, options: { encoding: 'utf8' }) => Promise<void>;
|
||||
now: () => Date;
|
||||
}) {
|
||||
return () => ({
|
||||
import type { createAppendToMpvLogHandler, createShowMpvOsdHandler } from './mpv-osd-log';
|
||||
|
||||
type AppendToMpvLogMainDeps = Parameters<typeof createAppendToMpvLogHandler>[0];
|
||||
type ShowMpvOsdMainDeps = Parameters<typeof createShowMpvOsdHandler>[0];
|
||||
|
||||
export function createBuildAppendToMpvLogMainDepsHandler(deps: AppendToMpvLogMainDeps) {
|
||||
return (): AppendToMpvLogMainDeps => ({
|
||||
logPath: deps.logPath,
|
||||
dirname: (targetPath: string) => deps.dirname(targetPath),
|
||||
mkdir: (targetPath: string, options: { recursive: boolean }) => deps.mkdir(targetPath, options),
|
||||
@@ -15,24 +14,12 @@ export function createBuildAppendToMpvLogMainDepsHandler(deps: {
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildShowMpvOsdMainDepsHandler(deps: {
|
||||
appendToMpvLog: (message: string) => void;
|
||||
showMpvOsdRuntime: (
|
||||
mpvClient: unknown | null,
|
||||
text: string,
|
||||
fallbackLog: (line: string) => void,
|
||||
) => void;
|
||||
getMpvClient: () => unknown | null;
|
||||
logInfo: (line: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
export function createBuildShowMpvOsdMainDepsHandler(deps: ShowMpvOsdMainDeps) {
|
||||
return (): ShowMpvOsdMainDeps => ({
|
||||
appendToMpvLog: (message: string) => deps.appendToMpvLog(message),
|
||||
showMpvOsdRuntime: (
|
||||
mpvClient: unknown | null,
|
||||
text: string,
|
||||
fallbackLog: (line: string) => void,
|
||||
) => deps.showMpvOsdRuntime(mpvClient, text, fallbackLog),
|
||||
getMpvClient: () => deps.getMpvClient() as never,
|
||||
showMpvOsdRuntime: (mpvClient, text, fallbackLog) =>
|
||||
deps.showMpvOsdRuntime(mpvClient, text, fallbackLog),
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
logInfo: (line: string) => deps.logInfo(line),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
import type { KikuFieldGroupingChoice } from '../../types';
|
||||
import { createOverlayRuntimeBootstrapHandlers } from './overlay-runtime-bootstrap-handlers';
|
||||
|
||||
test('overlay runtime bootstrap handlers compose options builder and bootstrap handler', () => {
|
||||
const appState = {
|
||||
backendOverride: null as string | null,
|
||||
windowTracker: null as unknown,
|
||||
windowTracker: null as BaseWindowTracker | null,
|
||||
subtitleTimingTracker: null as unknown,
|
||||
mpvClient: null as unknown,
|
||||
mpvClient: null,
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
runtimeOptionsManager: null as unknown,
|
||||
runtimeOptionsManager: null,
|
||||
ankiIntegration: null as unknown,
|
||||
};
|
||||
let initialized = false;
|
||||
@@ -39,7 +41,13 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h
|
||||
getOverlayWindows: () => [],
|
||||
getResolvedConfig: () => ({}),
|
||||
showDesktopNotification: () => {},
|
||||
createFieldGroupingCallback: () => (async () => 'combined' as never),
|
||||
createFieldGroupingCallback: () => async () =>
|
||||
({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
cancelled: true,
|
||||
}) as KikuFieldGroupingChoice,
|
||||
getKnownWordCacheStatePath: () => '/tmp/known.json',
|
||||
},
|
||||
initializeOverlayRuntimeBootstrapDeps: {
|
||||
|
||||
@@ -6,17 +6,24 @@ import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-ru
|
||||
type InitializeOverlayRuntimeMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeMainDepsHandler
|
||||
>[0];
|
||||
type InitializeOverlayRuntimeOptions = ReturnType<
|
||||
ReturnType<typeof createBuildInitializeOverlayRuntimeOptionsHandler>
|
||||
>;
|
||||
type InitializeOverlayRuntimeBootstrapMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler
|
||||
typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<InitializeOverlayRuntimeOptions>
|
||||
>[0];
|
||||
|
||||
export function createOverlayRuntimeBootstrapHandlers(deps: {
|
||||
initializeOverlayRuntimeMainDeps: InitializeOverlayRuntimeMainDeps;
|
||||
initializeOverlayRuntimeBootstrapDeps: Omit<InitializeOverlayRuntimeBootstrapMainDeps, 'buildOptions'>;
|
||||
initializeOverlayRuntimeBootstrapDeps: Omit<
|
||||
InitializeOverlayRuntimeBootstrapMainDeps,
|
||||
'buildOptions'
|
||||
>;
|
||||
}) {
|
||||
const buildInitializeOverlayRuntimeOptionsHandler = createBuildInitializeOverlayRuntimeOptionsHandler(
|
||||
createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(),
|
||||
);
|
||||
const buildInitializeOverlayRuntimeOptionsHandler =
|
||||
createBuildInitializeOverlayRuntimeOptionsHandler(
|
||||
createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(),
|
||||
);
|
||||
const initializeOverlayRuntime = createInitializeOverlayRuntimeHandler(
|
||||
createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler({
|
||||
...deps.initializeOverlayRuntimeBootstrapDeps,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-runtime-options-main-deps';
|
||||
|
||||
test('overlay runtime main deps builder maps runtime state and callbacks', () => {
|
||||
const calls: string[] = [];
|
||||
const appState = {
|
||||
backendOverride: 'x11' as string | null,
|
||||
windowTracker: null as unknown,
|
||||
windowTracker: null as BaseWindowTracker | null,
|
||||
subtitleTimingTracker: { id: 'tracker' } as unknown,
|
||||
mpvClient: null as { send?: (payload: { command: string[] }) => void } | null,
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
@@ -36,7 +37,12 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
getOverlayWindows: () => [],
|
||||
getResolvedConfig: () => ({}),
|
||||
showDesktopNotification: () => calls.push('notify'),
|
||||
createFieldGroupingCallback: () => async () => ({ cancelled: true }),
|
||||
createFieldGroupingCallback: () => async () => ({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
cancelled: true,
|
||||
}),
|
||||
getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json',
|
||||
});
|
||||
|
||||
@@ -58,7 +64,11 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
deps.syncOverlayShortcuts();
|
||||
deps.showDesktopNotification('title', {});
|
||||
|
||||
deps.setWindowTracker({ id: 'tracker' });
|
||||
const tracker = {
|
||||
close: () => {},
|
||||
getWindowGeometry: () => null,
|
||||
} as unknown as BaseWindowTracker;
|
||||
deps.setWindowTracker(tracker);
|
||||
deps.setAnkiIntegration({ id: 'anki' });
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
@@ -72,6 +82,6 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
'sync-shortcuts',
|
||||
'notify',
|
||||
]);
|
||||
assert.deepEqual(appState.windowTracker, { id: 'tracker' });
|
||||
assert.equal(appState.windowTracker, tracker);
|
||||
assert.deepEqual(appState.ankiIntegration, { id: 'anki' });
|
||||
});
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { AnkiConnectConfig } from '../../types';
|
||||
import type { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options';
|
||||
|
||||
type OverlayRuntimeOptionsMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeOptionsHandler
|
||||
>[0];
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
appState: {
|
||||
backendOverride: string | null;
|
||||
windowTracker: unknown | null;
|
||||
subtitleTimingTracker: unknown | null;
|
||||
mpvClient: unknown | null;
|
||||
windowTracker: Parameters<OverlayRuntimeOptionsMainDeps['setWindowTracker']>[0];
|
||||
subtitleTimingTracker: ReturnType<OverlayRuntimeOptionsMainDeps['getSubtitleTimingTracker']>;
|
||||
mpvClient: ReturnType<OverlayRuntimeOptionsMainDeps['getMpvClient']>;
|
||||
mpvSocketPath: string;
|
||||
runtimeOptionsManager: unknown | null;
|
||||
ankiIntegration: unknown | null;
|
||||
runtimeOptionsManager: ReturnType<OverlayRuntimeOptionsMainDeps['getRuntimeOptionsManager']>;
|
||||
ankiIntegration: Parameters<OverlayRuntimeOptionsMainDeps['setAnkiIntegration']>[0];
|
||||
};
|
||||
overlayManager: {
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
@@ -25,27 +30,36 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
createMainWindow: () => void;
|
||||
createInvisibleWindow: () => void;
|
||||
registerGlobalShortcuts: () => void;
|
||||
updateVisibleOverlayBounds: (geometry: { x: number; y: number; width: number; height: number }) => void;
|
||||
updateVisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => void;
|
||||
updateInvisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => void;
|
||||
getOverlayWindows: () => unknown[];
|
||||
getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows'];
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
createFieldGroupingCallback: () => unknown;
|
||||
createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback'];
|
||||
getKnownWordCacheStatePath: () => string;
|
||||
}) {
|
||||
return () => ({
|
||||
return (): OverlayRuntimeOptionsMainDeps => ({
|
||||
getBackendOverride: () => deps.appState.backendOverride,
|
||||
getInitialInvisibleOverlayVisibility: () => deps.getInitialInvisibleOverlayVisibility(),
|
||||
createMainWindow: () => deps.createMainWindow(),
|
||||
createInvisibleWindow: () => deps.createInvisibleWindow(),
|
||||
registerGlobalShortcuts: () => deps.registerGlobalShortcuts(),
|
||||
updateVisibleOverlayBounds: (geometry: { x: number; y: number; width: number; height: number }) =>
|
||||
deps.updateVisibleOverlayBounds(geometry),
|
||||
updateVisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => deps.updateVisibleOverlayBounds(geometry),
|
||||
updateInvisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -54,28 +68,25 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
}) => deps.updateInvisibleOverlayBounds(geometry),
|
||||
isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(),
|
||||
isInvisibleOverlayVisible: () => deps.overlayManager.getInvisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () => deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
updateVisibleOverlayVisibility: () =>
|
||||
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () =>
|
||||
deps.overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||
getOverlayWindows: () => deps.getOverlayWindows() as never,
|
||||
getOverlayWindows: () => deps.getOverlayWindows(),
|
||||
syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||
setWindowTracker: (tracker: unknown | null) => {
|
||||
setWindowTracker: (tracker) => {
|
||||
deps.appState.windowTracker = tracker;
|
||||
},
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
getSubtitleTimingTracker: () => deps.appState.subtitleTimingTracker,
|
||||
getMpvClient: () =>
|
||||
(deps.appState.mpvClient as { send?: (payload: { command: string[] }) => void } | null),
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
getMpvSocketPath: () => deps.appState.mpvSocketPath,
|
||||
getRuntimeOptionsManager: () =>
|
||||
deps.appState.runtimeOptionsManager as
|
||||
| { getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig }
|
||||
| null,
|
||||
setAnkiIntegration: (integration: unknown | null) => {
|
||||
getRuntimeOptionsManager: () => deps.appState.runtimeOptionsManager,
|
||||
setAnkiIntegration: (integration) => {
|
||||
deps.appState.ankiIntegration = integration;
|
||||
},
|
||||
showDesktopNotification: deps.showDesktopNotification,
|
||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback() as never,
|
||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
WindowGeometry,
|
||||
} from '../../types';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
|
||||
type OverlayRuntimeOptions = {
|
||||
backendOverride: string | null;
|
||||
@@ -20,7 +21,7 @@ type OverlayRuntimeOptions = {
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
syncOverlayShortcuts: () => void;
|
||||
setWindowTracker: (tracker: unknown | null) => void;
|
||||
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
getSubtitleTimingTracker: () => unknown | null;
|
||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||
@@ -50,7 +51,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
syncOverlayShortcuts: () => void;
|
||||
setWindowTracker: (tracker: unknown | null) => void;
|
||||
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
getSubtitleTimingTracker: () => unknown | null;
|
||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './overlay-visibility-runtime-main-deps';
|
||||
@@ -7,13 +9,14 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
|
||||
let trackerNotReadyWarningShown = false;
|
||||
const mainWindow = { id: 'main' } as never;
|
||||
const invisibleWindow = { id: 'invisible' } as never;
|
||||
const tracker = { id: 'tracker' } as unknown as BaseWindowTracker;
|
||||
|
||||
const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({
|
||||
getMainWindow: () => mainWindow,
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
getVisibleOverlayVisible: () => true,
|
||||
getInvisibleOverlayVisible: () => false,
|
||||
getWindowTracker: () => ({ id: 'tracker' }),
|
||||
getWindowTracker: () => tracker,
|
||||
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
|
||||
setTrackerNotReadyWarningShown: (shown) => {
|
||||
trackerNotReadyWarningShown = shown;
|
||||
|
||||
@@ -2,29 +2,19 @@ import type { BrowserWindow } from 'electron';
|
||||
import type { WindowGeometry } from '../../types';
|
||||
import type { OverlayVisibilityRuntimeDeps } from '../overlay-visibility-runtime';
|
||||
|
||||
export function createBuildOverlayVisibilityRuntimeMainDepsHandler(deps: {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
getWindowTracker: () => unknown | null;
|
||||
getTrackerNotReadyWarningShown: () => boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||
enforceOverlayLayerOrder: () => void;
|
||||
syncOverlayShortcuts: () => void;
|
||||
}) {
|
||||
export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
|
||||
deps: OverlayVisibilityRuntimeDeps,
|
||||
) {
|
||||
return (): OverlayVisibilityRuntimeDeps => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getInvisibleWindow: () => deps.getInvisibleWindow(),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
|
||||
getWindowTracker: () => deps.getWindowTracker() as never,
|
||||
getWindowTracker: () => deps.getWindowTracker(),
|
||||
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.updateVisibleOverlayBounds(geometry),
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.updateInvisibleOverlayBounds(geometry),
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
test('reload config main deps builder maps callbacks and fail handlers', async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildReloadConfigMainDepsHandler({
|
||||
reloadConfigStrict: () => ({ ok: true }),
|
||||
reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
|
||||
logInfo: (message) => calls.push(`info:${message}`),
|
||||
logWarning: (message) => calls.push(`warn:${message}`),
|
||||
showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`),
|
||||
@@ -24,7 +24,11 @@ test('reload config main deps builder maps callbacks and fail handlers', async (
|
||||
},
|
||||
})();
|
||||
|
||||
assert.deepEqual(deps.reloadConfigStrict(), { ok: true });
|
||||
assert.deepEqual(deps.reloadConfigStrict(), {
|
||||
ok: true,
|
||||
path: '/tmp/config.jsonc',
|
||||
warnings: [],
|
||||
});
|
||||
deps.logInfo('x');
|
||||
deps.logWarning('y');
|
||||
deps.showDesktopNotification('SubMiner', { body: 'warn' });
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
export function createBuildReloadConfigMainDepsHandler(deps: {
|
||||
reloadConfigStrict: () => unknown;
|
||||
logInfo: (message: string) => void;
|
||||
logWarning: (message: string) => void;
|
||||
showDesktopNotification: (title: string, options: { body: string }) => void;
|
||||
startConfigHotReload: () => void;
|
||||
refreshAnilistClientSecretState: (options: { force: boolean }) => Promise<unknown>;
|
||||
failHandlers: {
|
||||
logError: (details: string) => void;
|
||||
showErrorBox: (title: string, details: string) => void;
|
||||
quit: () => void;
|
||||
};
|
||||
}) {
|
||||
return () => ({
|
||||
reloadConfigStrict: () => deps.reloadConfigStrict() as never,
|
||||
import type { createCriticalConfigErrorHandler, createReloadConfigHandler } from './startup-config';
|
||||
|
||||
type ReloadConfigMainDeps = Parameters<typeof createReloadConfigHandler>[0];
|
||||
type CriticalConfigErrorMainDeps = Parameters<typeof createCriticalConfigErrorHandler>[0];
|
||||
|
||||
export function createBuildReloadConfigMainDepsHandler(deps: ReloadConfigMainDeps) {
|
||||
return (): ReloadConfigMainDeps => ({
|
||||
reloadConfigStrict: () => deps.reloadConfigStrict(),
|
||||
logInfo: (message: string) => deps.logInfo(message),
|
||||
logWarning: (message: string) => deps.logWarning(message),
|
||||
showDesktopNotification: (title: string, options: { body: string }) =>
|
||||
@@ -22,25 +15,20 @@ export function createBuildReloadConfigMainDepsHandler(deps: {
|
||||
deps.refreshAnilistClientSecretState(options),
|
||||
failHandlers: {
|
||||
logError: (details: string) => deps.failHandlers.logError(details),
|
||||
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||
showErrorBox: (title: string, details: string) =>
|
||||
deps.failHandlers.showErrorBox(title, details),
|
||||
quit: () => deps.failHandlers.quit(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildCriticalConfigErrorMainDepsHandler(deps: {
|
||||
getConfigPath: () => string;
|
||||
failHandlers: {
|
||||
logError: (details: string) => void;
|
||||
showErrorBox: (title: string, details: string) => void;
|
||||
quit: () => void;
|
||||
};
|
||||
}) {
|
||||
return () => ({
|
||||
export function createBuildCriticalConfigErrorMainDepsHandler(deps: CriticalConfigErrorMainDeps) {
|
||||
return (): CriticalConfigErrorMainDeps => ({
|
||||
getConfigPath: () => deps.getConfigPath(),
|
||||
failHandlers: {
|
||||
logError: (details: string) => deps.failHandlers.logError(details),
|
||||
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||
showErrorBox: (title: string, details: string) =>
|
||||
deps.failHandlers.showErrorBox(title, details),
|
||||
quit: () => deps.failHandlers.quit(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
test('tokenizer deps builder records known-word lookups and maps readers', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildTokenizerDepsMainHandler({
|
||||
getYomitanExt: () => ({ id: 'ext' }),
|
||||
getYomitanParserWindow: () => ({ id: 'window' }),
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => calls.push('set-window'),
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => calls.push('set-ready'),
|
||||
@@ -18,22 +18,22 @@ test('tokenizer deps builder records known-word lookups and maps readers', () =>
|
||||
setYomitanParserInitPromise: () => calls.push('set-init'),
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: (hit) => calls.push(`lookup:${hit}`),
|
||||
getKnownWordMatchMode: () => 'exact',
|
||||
getKnownWordMatchMode: () => 'surface',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => 'N2',
|
||||
getJlptEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyRank: () => 5,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
getMecabTokenizer: () => null,
|
||||
})();
|
||||
|
||||
assert.equal(deps.isKnownWord('known'), true);
|
||||
assert.equal(deps.isKnownWord('unknown'), false);
|
||||
deps.setYomitanParserWindow({});
|
||||
deps.setYomitanParserWindow(null);
|
||||
deps.setYomitanParserReadyPromise(null);
|
||||
deps.setYomitanParserInitPromise(null);
|
||||
assert.equal(deps.getMinSentenceWordsForNPlusOne(), 3);
|
||||
assert.equal(deps.getMinSentenceWordsForNPlusOne?.(), 3);
|
||||
assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
export function createBuildTokenizerDepsMainHandler(deps: {
|
||||
getYomitanExt: () => unknown;
|
||||
getYomitanParserWindow: () => unknown;
|
||||
setYomitanParserWindow: (window: unknown) => void;
|
||||
getYomitanParserReadyPromise: () => Promise<void> | null;
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||
getYomitanParserInitPromise: () => Promise<boolean> | null;
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
isKnownWord: (text: string) => boolean;
|
||||
import type { TokenizerDepsRuntimeOptions } from '../../core/services/tokenizer';
|
||||
|
||||
type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
|
||||
getJlptEnabled: NonNullable<TokenizerDepsRuntimeOptions['getJlptEnabled']>;
|
||||
getFrequencyDictionaryEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
|
||||
>;
|
||||
getFrequencyRank: NonNullable<TokenizerDepsRuntimeOptions['getFrequencyRank']>;
|
||||
getMinSentenceWordsForNPlusOne: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getMinSentenceWordsForNPlusOne']
|
||||
>;
|
||||
getYomitanGroupDebugEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getYomitanGroupDebugEnabled']
|
||||
>;
|
||||
recordLookup: (hit: boolean) => void;
|
||||
getKnownWordMatchMode: () => unknown;
|
||||
getMinSentenceWordsForNPlusOne: () => number;
|
||||
getJlptLevel: (text: string) => unknown;
|
||||
getJlptEnabled: () => boolean;
|
||||
getFrequencyDictionaryEnabled: () => boolean;
|
||||
getFrequencyRank: (text: string) => unknown;
|
||||
getYomitanGroupDebugEnabled: () => boolean;
|
||||
getMecabTokenizer: () => unknown;
|
||||
}) {
|
||||
return () => ({
|
||||
getYomitanExt: () => deps.getYomitanExt() as never,
|
||||
getYomitanParserWindow: () => deps.getYomitanParserWindow() as never,
|
||||
setYomitanParserWindow: (window: unknown) => deps.setYomitanParserWindow(window),
|
||||
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise() as never,
|
||||
};
|
||||
|
||||
export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
|
||||
return (): TokenizerDepsRuntimeOptions => ({
|
||||
getYomitanExt: () => deps.getYomitanExt(),
|
||||
getYomitanParserWindow: () => deps.getYomitanParserWindow(),
|
||||
setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window),
|
||||
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise(),
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) =>
|
||||
deps.setYomitanParserReadyPromise(promise),
|
||||
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise() as never,
|
||||
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise(),
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) =>
|
||||
deps.setYomitanParserInitPromise(promise),
|
||||
isKnownWord: (text: string) => {
|
||||
@@ -32,14 +31,14 @@ export function createBuildTokenizerDepsMainHandler(deps: {
|
||||
deps.recordLookup(hit);
|
||||
return hit;
|
||||
},
|
||||
getKnownWordMatchMode: () => deps.getKnownWordMatchMode() as never,
|
||||
getKnownWordMatchMode: () => deps.getKnownWordMatchMode(),
|
||||
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
|
||||
getJlptLevel: (text: string) => deps.getJlptLevel(text) as never,
|
||||
getJlptLevel: (text: string) => deps.getJlptLevel(text),
|
||||
getJlptEnabled: () => deps.getJlptEnabled(),
|
||||
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text) as never,
|
||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
|
||||
getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(),
|
||||
getMecabTokenizer: () => deps.getMecabTokenizer() as never,
|
||||
getMecabTokenizer: () => deps.getMecabTokenizer(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { createDestroyTrayHandler, createEnsureTrayHandler } from './tray-lifecycle';
|
||||
import { createBuildDestroyTrayMainDepsHandler, createBuildEnsureTrayMainDepsHandler } from './app-runtime-main-deps';
|
||||
import { createBuildTrayMenuTemplateHandler, createResolveTrayIconPathHandler } from './tray-main-actions';
|
||||
import {
|
||||
createBuildDestroyTrayMainDepsHandler,
|
||||
createBuildEnsureTrayMainDepsHandler,
|
||||
} from './app-runtime-main-deps';
|
||||
import {
|
||||
createBuildTrayMenuTemplateHandler,
|
||||
createResolveTrayIconPathHandler,
|
||||
} from './tray-main-actions';
|
||||
import {
|
||||
createBuildResolveTrayIconPathMainDepsHandler,
|
||||
createBuildTrayMenuTemplateMainDepsHandler,
|
||||
} from './tray-main-deps';
|
||||
|
||||
type ResolveTrayIconPathMainDeps = Parameters<typeof createBuildResolveTrayIconPathMainDepsHandler>[0];
|
||||
type ResolveTrayIconPathMainDeps = Parameters<
|
||||
typeof createBuildResolveTrayIconPathMainDepsHandler
|
||||
>[0];
|
||||
type BuildTrayMenuTemplateMainDeps<TMenuItem> = Parameters<
|
||||
typeof createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>
|
||||
>[0];
|
||||
type EnsureTrayMainDeps = Parameters<typeof createBuildEnsureTrayMainDepsHandler>[0];
|
||||
type DestroyTrayMainDeps = Parameters<typeof createBuildDestroyTrayMainDepsHandler>[0];
|
||||
type EnsureTrayMainDeps<TTrayMenu> = Parameters<
|
||||
typeof createBuildEnsureTrayMainDepsHandler<TrayLike, TTrayMenu, TrayIconLike>
|
||||
>[0];
|
||||
type TrayLike = NonNullable<ReturnType<Parameters<typeof createEnsureTrayHandler>[0]['getTray']>>;
|
||||
type TrayIconLike = Parameters<Parameters<typeof createEnsureTrayHandler>[0]['createTray']>[0];
|
||||
type DestroyTrayMainDeps = Parameters<typeof createBuildDestroyTrayMainDepsHandler<TrayLike>>[0];
|
||||
|
||||
export function createTrayRuntimeHandlers<TMenuItem, TMenu>(deps: {
|
||||
resolveTrayIconPathDeps: ResolveTrayIconPathMainDeps;
|
||||
buildTrayMenuTemplateDeps: BuildTrayMenuTemplateMainDeps<TMenuItem>;
|
||||
ensureTrayDeps: Omit<EnsureTrayMainDeps, 'buildTrayMenu' | 'resolveTrayIconPath'>;
|
||||
ensureTrayDeps: Omit<EnsureTrayMainDeps<TMenu>, 'buildTrayMenu' | 'resolveTrayIconPath'>;
|
||||
destroyTrayDeps: DestroyTrayMainDeps;
|
||||
buildMenuFromTemplate: (template: TMenuItem[]) => TMenu;
|
||||
}) {
|
||||
|
||||
Reference in New Issue
Block a user