diff --git a/CHANGELOG.md b/CHANGELOG.md index 24242c93..86936a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## v0.12.0 (2026-04-11) + +### Changed +- Overlay: Added configurable overlay shortcuts for session help, controller select, and controller debug actions. +- Overlay: Added mpv/plugin and CLI routing for session help, controller utilities, and subtitle sidebar toggling through the shared session-action path. +- Overlay: Improved dedicated overlay modal retry and focus handling for runtime options, Jimaku, session help, controller tools, and the playlist browser. +- Overlay: Fixed controller configuration and controller debug shortcut opens so configured bindings bring up their modals again instead of tripping renderer recovery. +- Stats: Sessions are rolled up per episode within each day, with a bulk delete that wipes every session in the group. +- Stats: Trends add a 365-day range next to the existing 7d/30d/90d/all options. +- Stats: Library detail view gets a delete-episode action that removes the video and all its sessions. +- Stats: Vocabulary Top 50 tightens the word/reading column so katakana entries no longer push the scores off screen. +- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity. +- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility. +- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard. +- Stats: Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds. +- Stats: Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector. + +### Fixed +- Overlay: Fixed overlay drag-and-drop routing so dropping external subtitle files like `.ass` onto mpv still loads them when the overlay is visible. +- Overlay: Addressed the latest CodeRabbit follow-ups on PR #49, including generation-scoped Lua session binding names, stricter session command validation, session-help shortcut visibility, the numeric-selection key guard, stats-overlay startup classification, and safer session-binding persistence. +- Overlay: Addressed the latest CodeRabbit follow-ups on the Windows overlay flow, including exact mpv target resolution, lower-overlay helper arguments, Win32 failure detection, and overlay cleanup on tracker loss. +- Overlay: Fixed Windows overlay z-order so the visible subtitle overlay stops staying above unrelated apps after mpv loses focus. +- Overlay: Fixed Windows overlay tracking to use native window polling and owner/z-order binding, which keeps the subtitle overlay aligned to the active mpv window more reliably. +- Overlay: Fixed Windows overlay hide/restore behavior so minimizing mpv immediately hides the overlay and restoring mpv brings it back on top of the mpv window without requiring a click. +- Overlay: Fixed stats overlay layering so the in-player stats page now stays above mpv and the subtitle overlay while it is open. +- Overlay: Fixed Windows subtitle overlay stability so transient tracker misses and restore events keep the current subtitle visible instead of waiting for the next subtitle line. +- Overlay: Fixed Windows focus handoff from the interactive subtitle overlay back to mpv so the overlay no longer drops behind mpv and briefly disappears. +- Overlay: Fixed Windows visible-overlay startup so it no longer briefly opens as an interactive or opaque surface before the tracked transparent overlay state settles. +- Overlay: Fixed spurious auto-pause after overlay visibility recovery and window resize so the overlay no longer pauses mpv until the pointer genuinely re-enters the subtitle area. +- Overlay: Fixed Windows secondary subtitle hover mode so the expanded hover hit area no longer blocks the native minimize, maximize, and close buttons. +- Overlay: Fixed Windows Yomitan popup focus loss after closing nested lookups so the original popup stays interactive instead of falling through to mpv. +- Stats: Fixed immersion-tracker timestamp handling under Bun/libsql so library rows, session timelines, and lifetime summaries keep real wall-clock millisecond values instead of truncating to invalid negative timestamps. +- Mpv Plugin: Fixed the mpv Lua plugin so hover and environment modules no longer use the `goto continue` pattern that can fail to parse on some user Lua runtimes. + +### Internal +- Release: Added a dedicated beta/rc prerelease GitHub Actions workflow that publishes GitHub prereleases without consuming pending changelog fragments or updating AUR. +- Release: Added prerelease note generation so beta and release-candidate tags can reuse the current pending `changes/*.md` fragments while leaving stable changelog publication for the final release cut. + ## v0.11.2 (2026-04-07) ### Changed diff --git a/backlog/tasks/task-290 - Cut-stable-release-v0.12.0-on-main.md b/backlog/tasks/task-290 - Cut-stable-release-v0.12.0-on-main.md new file mode 100644 index 00000000..0d418857 --- /dev/null +++ b/backlog/tasks/task-290 - Cut-stable-release-v0.12.0-on-main.md @@ -0,0 +1,56 @@ +--- +id: TASK-290 +title: Cut stable release v0.12.0 on main +status: Done +assignee: + - codex +created_date: '2026-04-12 04:47' +updated_date: '2026-04-12 04:51' +labels: [] +dependencies: [] +documentation: + - docs/RELEASING.md +priority: high +--- + +## Description + + +Prepare the main branch for the stable SubMiner v0.12.0 release by applying the release-version updates, formatting changes required by the branch state, and rerunning the full release verification gate. + + +## Acceptance Criteria + +- [x] #1 Main branch version and stable release metadata are updated for v0.12.0. +- [x] #2 Required formatting changes for the release candidate tree are applied and verified. +- [x] #3 The documented release verification gate passes locally and any remaining push or tag prerequisites are documented. + + +## Implementation Plan + + +1. Audit main-branch release state: package version, release artifacts, current CI status, and current formatting debt. +2. Apply required formatting fixes to the files reported by `bun run format:check:src` and verify the formatting lane passes. +3. Update the package version to 0.12.0 and generate stable release metadata (`CHANGELOG.md`, `release/release-notes.md`, `docs-site/changelog.md`) using the documented release workflow. +4. Run the full local release gate on main (`changelog:lint`, `changelog:check --version 0.12.0`, `verify:config-example`, `typecheck`, `test:fast`, `test:env`, `build`, `docs:test`, `docs:build`, plus dist smoke) and document any remaining tag/push prerequisites. + + +## Implementation Notes + + +Applied Prettier to all 39 files reported by `bun run format:check:src` on main and verified the formatting lane now passes. + +Reapplied the stable changelog build entrypoint fix on main: added `writeStableReleaseArtifacts`, covered it with a focused regression test, and updated `package.json` so `changelog:build` forwards `--version` and `--date` through a single `build-release` command. + +Verified the formatted mainline release tree with `bun run changelog:lint`, `bun run changelog:check --version 0.12.0`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run docs:test`, `bun run docs:build`, and `bun run test:smoke:dist`; all passed. + +Remote main CI also completed successfully for `Windows update (#49)` after the local release-prep pass. Remaining operational steps are commit/tag/push only. + + +## Final Summary + + +Prepared `main` for the stable `v0.12.0` cut. Formatted the previously failing source files so `bun run format:check:src` is now clean, bumped `package.json` from `0.12.0-beta.3` to `0.12.0`, and generated the stable release artifacts with the explicit local cut date `2026-04-11`, which consumed the pending changelog fragments into `CHANGELOG.md`, `docs-site/changelog.md`, and `release/release-notes.md`. + +Also reintroduced the release-script fix on main: the old `changelog:build` package script still used `build && docs`, which can drop `--version/--date` on the first step. Added a focused regression test in `scripts/build-changelog.test.ts`, implemented `writeStableReleaseArtifacts` in `scripts/build-changelog.ts`, and switched `package.json` to `build-release` so release flags propagate correctly. Verification on the final tree passed for formatting, changelog lint/check, config example verification, typecheck, fast tests, env tests, build, docs tests/build, dist smoke, and remote main CI. The branch is release-ready pending commit, tag, and push. + diff --git a/changes/2026-04-09-prerelease-workflow.md b/changes/2026-04-09-prerelease-workflow.md deleted file mode 100644 index 37941416..00000000 --- a/changes/2026-04-09-prerelease-workflow.md +++ /dev/null @@ -1,5 +0,0 @@ -type: internal -area: release - -- Added a dedicated beta/rc prerelease GitHub Actions workflow that publishes GitHub prereleases without consuming pending changelog fragments or updating AUR. -- Added prerelease note generation so beta and release-candidate tags can reuse the current pending `changes/*.md` fragments while leaving stable changelog publication for the final release cut. diff --git a/changes/fix-overlay-subtitle-drop-routing.md b/changes/fix-overlay-subtitle-drop-routing.md deleted file mode 100644 index 0711dee9..00000000 --- a/changes/fix-overlay-subtitle-drop-routing.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Fixed overlay drag-and-drop routing so dropping external subtitle files like `.ass` onto mpv still loads them when the overlay is visible. diff --git a/changes/fix-pr-49-coderabbit-review-follow-ups.md b/changes/fix-pr-49-coderabbit-review-follow-ups.md deleted file mode 100644 index 320cb661..00000000 --- a/changes/fix-pr-49-coderabbit-review-follow-ups.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Addressed the latest CodeRabbit follow-ups on PR #49, including generation-scoped Lua session binding names, stricter session command validation, session-help shortcut visibility, the numeric-selection key guard, stats-overlay startup classification, and safer session-binding persistence. diff --git a/changes/fix-windows-coderabbit-review-follow-ups.md b/changes/fix-windows-coderabbit-review-follow-ups.md deleted file mode 100644 index 46691491..00000000 --- a/changes/fix-windows-coderabbit-review-follow-ups.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Addressed the latest CodeRabbit follow-ups on the Windows overlay flow, including exact mpv target resolution, lower-overlay helper arguments, Win32 failure detection, and overlay cleanup on tracker loss. diff --git a/changes/fix-windows-overlay-z-order.md b/changes/fix-windows-overlay-z-order.md deleted file mode 100644 index d5b1f57e..00000000 --- a/changes/fix-windows-overlay-z-order.md +++ /dev/null @@ -1,11 +0,0 @@ -type: fixed -area: overlay - -- Fixed Windows overlay z-order so the visible subtitle overlay stops staying above unrelated apps after mpv loses focus. -- Fixed Windows overlay tracking to use native window polling and owner/z-order binding, which keeps the subtitle overlay aligned to the active mpv window more reliably. -- Fixed Windows overlay hide/restore behavior so minimizing mpv immediately hides the overlay and restoring mpv brings it back on top of the mpv window without requiring a click. -- Fixed stats overlay layering so the in-player stats page now stays above mpv and the subtitle overlay while it is open. -- Fixed Windows subtitle overlay stability so transient tracker misses and restore events keep the current subtitle visible instead of waiting for the next subtitle line. -- Fixed Windows focus handoff from the interactive subtitle overlay back to mpv so the overlay no longer drops behind mpv and briefly disappears. -- Fixed Windows visible-overlay startup so it no longer briefly opens as an interactive or opaque surface before the tracked transparent overlay state settles. -- Fixed spurious auto-pause after overlay visibility recovery and window resize so the overlay no longer pauses mpv until the pointer genuinely re-enters the subtitle area. diff --git a/changes/fix-windows-secondary-hover-titlebar.md b/changes/fix-windows-secondary-hover-titlebar.md deleted file mode 100644 index 94890fba..00000000 --- a/changes/fix-windows-secondary-hover-titlebar.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Fixed Windows secondary subtitle hover mode so the expanded hover hit area no longer blocks the native minimize, maximize, and close buttons. diff --git a/changes/fix-yomitan-nested-popup-focus.md b/changes/fix-yomitan-nested-popup-focus.md deleted file mode 100644 index 1a343cae..00000000 --- a/changes/fix-yomitan-nested-popup-focus.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Fixed Windows Yomitan popup focus loss after closing nested lookups so the original popup stays interactive instead of falling through to mpv. diff --git a/changes/honor-configured-controller-shortcuts-and-modal-routing.md b/changes/honor-configured-controller-shortcuts-and-modal-routing.md deleted file mode 100644 index 4e5d98d7..00000000 --- a/changes/honor-configured-controller-shortcuts-and-modal-routing.md +++ /dev/null @@ -1,7 +0,0 @@ -type: changed -area: overlay - -- Added configurable overlay shortcuts for session help, controller select, and controller debug actions. -- Added mpv/plugin and CLI routing for session help, controller utilities, and subtitle sidebar toggling through the shared session-action path. -- Improved dedicated overlay modal retry and focus handling for runtime options, Jimaku, session help, controller tools, and the playlist browser. -- Fixed controller configuration and controller debug shortcut opens so configured bindings bring up their modals again instead of tripping renderer recovery. diff --git a/changes/immersion-tracker-timestamp-compat.md b/changes/immersion-tracker-timestamp-compat.md deleted file mode 100644 index c1cffa9f..00000000 --- a/changes/immersion-tracker-timestamp-compat.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: stats - -- Fixed immersion-tracker timestamp handling under Bun/libsql so library rows, session timelines, and lifetime summaries keep real wall-clock millisecond values instead of truncating to invalid negative timestamps. diff --git a/changes/lua-plugin-parser-compat.md b/changes/lua-plugin-parser-compat.md deleted file mode 100644 index 70dc19ea..00000000 --- a/changes/lua-plugin-parser-compat.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: mpv-plugin - -- Fixed the mpv Lua plugin so hover and environment modules no longer use the `goto continue` pattern that can fail to parse on some user Lua runtimes. diff --git a/changes/stats-dashboard-feedback-pass.md b/changes/stats-dashboard-feedback-pass.md deleted file mode 100644 index 83bbbbcf..00000000 --- a/changes/stats-dashboard-feedback-pass.md +++ /dev/null @@ -1,11 +0,0 @@ -type: changed -area: stats - -- Sessions are rolled up per episode within each day, with a bulk delete that wipes every session in the group. -- Trends add a 365-day range next to the existing 7d/30d/90d/all options. -- Library detail view gets a delete-episode action that removes the video and all its sessions. -- Vocabulary Top 50 tightens the word/reading column so katakana entries no longer push the scores off screen. -- Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity. -- Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility. -- Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard. -- Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds. diff --git a/changes/stats-library-summary.md b/changes/stats-library-summary.md deleted file mode 100644 index b20beabc..00000000 --- a/changes/stats-library-summary.md +++ /dev/null @@ -1,4 +0,0 @@ -type: changed -area: stats - -- Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector. diff --git a/docs-site/changelog.md b/docs-site/changelog.md index baf85e19..a95b4718 100644 --- a/docs-site/changelog.md +++ b/docs-site/changelog.md @@ -1,6 +1,49 @@ # Changelog -## v0.11.2 (2026-04-07) +## v0.12.0 (2026-04-11) + +**Changed** +- Overlay: Added configurable overlay shortcuts for session help, controller select, and controller debug actions. +- Overlay: Added mpv/plugin and CLI routing for session help, controller utilities, and subtitle sidebar toggling through the shared session-action path. +- Overlay: Improved dedicated overlay modal retry and focus handling for runtime options, Jimaku, session help, controller tools, and the playlist browser. +- Overlay: Fixed controller configuration and controller debug shortcut opens so configured bindings bring up their modals again instead of tripping renderer recovery. +- Stats: Sessions are rolled up per episode within each day, with a bulk delete that wipes every session in the group. +- Stats: Trends add a 365-day range next to the existing 7d/30d/90d/all options. +- Stats: Library detail view gets a delete-episode action that removes the video and all its sessions. +- Stats: Vocabulary Top 50 tightens the word/reading column so katakana entries no longer push the scores off screen. +- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity. +- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility. +- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard. +- Stats: Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds. +- Stats: Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector. + +**Fixed** +- Overlay: Fixed overlay drag-and-drop routing so dropping external subtitle files like `.ass` onto mpv still loads them when the overlay is visible. +- Overlay: Addressed the latest CodeRabbit follow-ups on PR #49, including generation-scoped Lua session binding names, stricter session command validation, session-help shortcut visibility, the numeric-selection key guard, stats-overlay startup classification, and safer session-binding persistence. +- Overlay: Addressed the latest CodeRabbit follow-ups on the Windows overlay flow, including exact mpv target resolution, lower-overlay helper arguments, Win32 failure detection, and overlay cleanup on tracker loss. +- Overlay: Fixed Windows overlay z-order so the visible subtitle overlay stops staying above unrelated apps after mpv loses focus. +- Overlay: Fixed Windows overlay tracking to use native window polling and owner/z-order binding, which keeps the subtitle overlay aligned to the active mpv window more reliably. +- Overlay: Fixed Windows overlay hide/restore behavior so minimizing mpv immediately hides the overlay and restoring mpv brings it back on top of the mpv window without requiring a click. +- Overlay: Fixed stats overlay layering so the in-player stats page now stays above mpv and the subtitle overlay while it is open. +- Overlay: Fixed Windows subtitle overlay stability so transient tracker misses and restore events keep the current subtitle visible instead of waiting for the next subtitle line. +- Overlay: Fixed Windows focus handoff from the interactive subtitle overlay back to mpv so the overlay no longer drops behind mpv and briefly disappears. +- Overlay: Fixed Windows visible-overlay startup so it no longer briefly opens as an interactive or opaque surface before the tracked transparent overlay state settles. +- Overlay: Fixed spurious auto-pause after overlay visibility recovery and window resize so the overlay no longer pauses mpv until the pointer genuinely re-enters the subtitle area. +- Overlay: Fixed Windows secondary subtitle hover mode so the expanded hover hit area no longer blocks the native minimize, maximize, and close buttons. +- Overlay: Fixed Windows Yomitan popup focus loss after closing nested lookups so the original popup stays interactive instead of falling through to mpv. +- Stats: Fixed immersion-tracker timestamp handling under Bun/libsql so library rows, session timelines, and lifetime summaries keep real wall-clock millisecond values instead of truncating to invalid negative timestamps. +- Mpv Plugin: Fixed the mpv Lua plugin so hover and environment modules no longer use the `goto continue` pattern that can fail to parse on some user Lua runtimes. + +**Internal** +- Release: Added a dedicated beta/rc prerelease GitHub Actions workflow that publishes GitHub prereleases without consuming pending changelog fragments or updating AUR. +- Release: Added prerelease note generation so beta and release-candidate tags can reuse the current pending `changes/*.md` fragments while leaving stable changelog publication for the final release cut. + +## Previous Versions + +
+v0.11.x + +

v0.11.2 (2026-04-07)

**Changed** - Launcher: Replaced the launcher-only fullscreen toggle with `mpv.launchMode` so SubMiner-managed mpv playback can start in normal, maximized, or fullscreen mode. @@ -10,13 +53,13 @@ - Launcher: Local playback now promotes a single unlabeled external subtitle sidecar to the primary slot instead of leaving mpv's embedded English auto-selection in place. - Release: Fixed Linux AppImage startup packaging so Chromium child relaunches can resolve the bundled `libffmpeg.so` instead of crash-looping on startup. -## v0.11.1 (2026-04-04) +

v0.11.1 (2026-04-04)

**Fixed** - Release: Linux packaged builds now expose the canonical `SubMiner` app identity to Electron's startup metadata so native Wayland compositors stop reporting the window class/app-id as lowercase `subminer`. - Linux: Linux now restores the runtime options, Jimaku, and Subsync shortcuts after the Electron 39 regression by routing those actions through the overlay's mpv/IPC shortcut path. -## v0.11.0 (2026-04-03) +

v0.11.0 (2026-04-03)

**Added** - Overlay: Added a playlist browser overlay modal for browsing sibling video files and the live mpv queue during playback. @@ -69,7 +112,7 @@ - Release: Kept GitHub Releases green when AUR publish flakes and needs manual follow-up. - Release: Updated Electron to 39.8.6 and pinned patched transitive build dependencies to clear the reported high-severity audit findings. -## Previous Versions +
v0.10.x diff --git a/package.json b/package.json index 294c2d5d..9239dbb7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "subminer", "productName": "SubMiner", "desktopName": "SubMiner.desktop", - "version": "0.12.0-beta.3", + "version": "0.12.0", "description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration", "packageManager": "bun@1.3.5", "main": "dist/main-entry.js", @@ -20,7 +20,7 @@ "dev:stats": "cd stats && bun run dev", "build": "bun run build:yomitan && bun run build:stats && tsc -p tsconfig.json && bun run build:renderer && bun run build:launcher && bun run build:assets", "build:renderer": "esbuild src/renderer/renderer.ts --bundle --platform=browser --format=esm --target=es2022 --outfile=dist/renderer/renderer.js --sourcemap", - "changelog:build": "bun run scripts/build-changelog.ts build && bun run changelog:docs", + "changelog:build": "bun run scripts/build-changelog.ts build-release", "changelog:check": "bun run scripts/build-changelog.ts check", "changelog:docs": "bun run scripts/build-changelog.ts docs", "changelog:lint": "bun run scripts/build-changelog.ts lint", diff --git a/scripts/build-changelog.test.ts b/scripts/build-changelog.test.ts index 9c835870..f8d91bf2 100644 --- a/scripts/build-changelog.test.ts +++ b/scripts/build-changelog.test.ts @@ -139,6 +139,49 @@ test('writeChangelogArtifacts skips changelog prepend when release section alrea } }); +test('writeStableReleaseArtifacts reuses the requested version and date for changelog, release notes, and docs-site output', async () => { + const { writeStableReleaseArtifacts } = await loadModule(); + const workspace = createWorkspace('write-stable-release-artifacts'); + const projectRoot = path.join(workspace, 'SubMiner'); + + fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'docs-site'), { recursive: true }); + fs.writeFileSync( + path.join(projectRoot, 'package.json'), + JSON.stringify({ name: 'subminer', version: '0.4.1' }, null, 2), + 'utf8', + ); + fs.writeFileSync(path.join(projectRoot, 'CHANGELOG.md'), '# Changelog\n', 'utf8'); + fs.writeFileSync( + path.join(projectRoot, 'changes', '001.md'), + ['type: fixed', 'area: release', '', '- Reused explicit stable release date.'].join('\n'), + 'utf8', + ); + + try { + const result = writeStableReleaseArtifacts({ + cwd: projectRoot, + version: '0.4.1', + date: '2026-03-07', + }); + + assert.deepEqual(result.outputPaths, [path.join(projectRoot, 'CHANGELOG.md')]); + assert.equal(result.releaseNotesPath, path.join(projectRoot, 'release', 'release-notes.md')); + assert.equal(result.docsChangelogPath, path.join(projectRoot, 'docs-site', 'changelog.md')); + + const changelog = fs.readFileSync(path.join(projectRoot, 'CHANGELOG.md'), 'utf8'); + const docsChangelog = fs.readFileSync( + path.join(projectRoot, 'docs-site', 'changelog.md'), + 'utf8', + ); + + assert.match(changelog, /## v0\.4\.1 \(2026-03-07\)/); + assert.match(docsChangelog, /## v0\.4\.1 \(2026-03-07\)/); + } finally { + fs.rmSync(workspace, { recursive: true, force: true }); + } +}); + test('verifyChangelogReadyForRelease ignores README but rejects pending fragments and missing version sections', async () => { const { verifyChangelogReadyForRelease } = await loadModule(); const workspace = createWorkspace('verify-release'); @@ -362,11 +405,11 @@ test('writePrereleaseNotesForVersion writes cumulative beta notes without mutati const prereleaseNotes = fs.readFileSync(outputPath, 'utf8'); assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m); - assert.match(prereleaseNotes, /## Highlights\n### Added\n- Overlay: Added prerelease coverage\./); assert.match( prereleaseNotes, - /### Fixed\n- Launcher: Fixed prerelease packaging checks\./, + /## Highlights\n### Added\n- Overlay: Added prerelease coverage\./, ); + assert.match(prereleaseNotes, /### Fixed\n- Launcher: Fixed prerelease packaging checks\./); assert.match(prereleaseNotes, /## Installation\n\nSee the README and docs\/installation guide/); } finally { fs.rmSync(workspace, { recursive: true, force: true }); diff --git a/scripts/build-changelog.ts b/scripts/build-changelog.ts index c5fe4158..be843b58 100644 --- a/scripts/build-changelog.ts +++ b/scripts/build-changelog.ts @@ -430,6 +430,21 @@ export function writeChangelogArtifacts(options?: ChangelogOptions): { }; } +export function writeStableReleaseArtifacts(options?: ChangelogOptions): { + deletedFragmentPaths: string[]; + docsChangelogPath: string; + outputPaths: string[]; + releaseNotesPath: string; +} { + const changelogResult = writeChangelogArtifacts(options); + const docsChangelogPath = generateDocsChangelog(options); + + return { + ...changelogResult, + docsChangelogPath, + }; +} + export function verifyChangelogFragments(options?: ChangelogOptions): void { readChangeFragments(options?.cwd ?? process.cwd(), options?.deps); } @@ -726,6 +741,11 @@ function main(): void { return; } + if (command === 'build-release') { + writeStableReleaseArtifacts(options); + return; + } + if (command === 'check') { verifyChangelogReadyForRelease(options); return; diff --git a/src/cli/args.test.ts b/src/cli/args.test.ts index 9df171d0..5047f5ca 100644 --- a/src/cli/args.test.ts +++ b/src/cli/args.test.ts @@ -107,11 +107,7 @@ test('parseArgs captures session action forwarding flags', () => { }); test('parseArgs ignores non-positive numeric session action counts', () => { - const args = parseArgs([ - '--copy-subtitle-count=0', - '--mine-sentence-count', - '-1', - ]); + const args = parseArgs(['--copy-subtitle-count=0', '--mine-sentence-count', '-1']); assert.equal(args.copySubtitleCount, undefined); assert.equal(args.mineSentenceCount, undefined); @@ -221,10 +217,7 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => { assert.equal(hasExplicitCommand(toggleStatsOverlay), true); assert.equal(shouldStartApp(toggleStatsOverlay), true); - const cycleRuntimeOption = parseArgs([ - '--cycle-runtime-option', - 'anki.autoUpdateNewCards:next', - ]); + const cycleRuntimeOption = parseArgs(['--cycle-runtime-option', 'anki.autoUpdateNewCards:next']); assert.equal(cycleRuntimeOption.cycleRuntimeOptionId, 'anki.autoUpdateNewCards'); assert.equal(cycleRuntimeOption.cycleRuntimeOptionDirection, 1); assert.equal(hasExplicitCommand(cycleRuntimeOption), true); diff --git a/src/cli/args.ts b/src/cli/args.ts index 32a3d593..2df3e301 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -173,7 +173,10 @@ export function parseArgs(argv: string[]): CliArgs { const separatorIndex = value.lastIndexOf(':'); if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null; const id = value.slice(0, separatorIndex).trim(); - const rawDirection = value.slice(separatorIndex + 1).trim().toLowerCase(); + const rawDirection = value + .slice(separatorIndex + 1) + .trim() + .toLowerCase(); if (!id) return null; if (rawDirection === 'next' || rawDirection === '1') { return { id, direction: 1 }; diff --git a/src/core/services/app-ready.test.ts b/src/core/services/app-ready.test.ts index a98d359e..d1a225e6 100644 --- a/src/core/services/app-ready.test.ts +++ b/src/core/services/app-ready.test.ts @@ -75,9 +75,7 @@ test('runAppReadyRuntime starts websocket in auto mode when plugin missing', asy calls.indexOf('setVisibleOverlayVisible:true') < calls.indexOf('initializeOverlayRuntime'), ); assert.ok(calls.includes('startBackgroundWarmups')); - assert.ok( - calls.includes('log:Runtime ready: immersion tracker startup requested.'), - ); + assert.ok(calls.includes('log:Runtime ready: immersion tracker startup requested.')); }); test('runAppReadyRuntime starts texthooker on startup when enabled in config', async () => { diff --git a/src/core/services/cli-command.ts b/src/core/services/cli-command.ts index cdd61f44..85ba0168 100644 --- a/src/core/services/cli-command.ts +++ b/src/core/services/cli-command.ts @@ -277,12 +277,7 @@ export function handleCliCommand( logLabel: string, osdLabel: string, ): void => { - runAsyncWithOsd( - () => deps.dispatchSessionAction(request), - deps, - logLabel, - osdLabel, - ); + runAsyncWithOsd(() => deps.dispatchSessionAction(request), deps, logLabel, osdLabel); }; if (args.logLevel) { diff --git a/src/core/services/immersion-tracker/__tests__/query.test.ts b/src/core/services/immersion-tracker/__tests__/query.test.ts index 6b258d7d..fcc18a7f 100644 --- a/src/core/services/immersion-tracker/__tests__/query.test.ts +++ b/src/core/services/immersion-tracker/__tests__/query.test.ts @@ -3840,16 +3840,7 @@ test('getTrendsDashboard builds librarySummary with per-title aggregates', () => lines_seen = ?, tokens_seen = ?, cards_mined = ?, yomitan_lookup_count = ? WHERE session_id = ? `, - ).run( - `${startedAtMs + activeMs}`, - activeMs, - activeMs, - 10, - tokens, - cards, - lookups, - sessionId, - ); + ).run(`${startedAtMs + activeMs}`, activeMs, activeMs, 10, tokens, cards, lookups, sessionId); } for (const [day, active, tokens, cards] of [ @@ -3947,16 +3938,7 @@ test('getTrendsDashboard librarySummary returns null lookupsPerHundred when word lines_seen = ?, tokens_seen = ?, cards_mined = ?, yomitan_lookup_count = ? WHERE session_id = ? `, - ).run( - `${startMs + 20 * 60_000}`, - 20 * 60_000, - 20 * 60_000, - 5, - 0, - 0, - 0, - session.sessionId, - ); + ).run(`${startMs + 20 * 60_000}`, 20 * 60_000, 20 * 60_000, 5, 0, 0, 0, session.sessionId); db.prepare( ` diff --git a/src/core/services/immersion-tracker/query-trends.ts b/src/core/services/immersion-tracker/query-trends.ts index f521886e..8dd4aecd 100644 --- a/src/core/services/immersion-tracker/query-trends.ts +++ b/src/core/services/immersion-tracker/query-trends.ts @@ -414,8 +414,7 @@ function buildLibrarySummary( cards: acc.cards, words: acc.words, lookups: acc.lookups, - lookupsPerHundred: - acc.words > 0 ? +((acc.lookups / acc.words) * 100).toFixed(1) : null, + lookupsPerHundred: acc.words > 0 ? +((acc.lookups / acc.words) * 100).toFixed(1) : null, firstWatched: acc.firstWatched, lastWatched: acc.lastWatched, }); diff --git a/src/core/services/immersion-tracker/storage-session.test.ts b/src/core/services/immersion-tracker/storage-session.test.ts index 2dde75fd..d07dc7a6 100644 --- a/src/core/services/immersion-tracker/storage-session.test.ts +++ b/src/core/services/immersion-tracker/storage-session.test.ts @@ -606,13 +606,15 @@ test('ensureSchema migrates session event timestamps to text and repairs libsql- }>; assert.equal(column.find((entry) => entry.name === 'ts_ms')?.type, 'TEXT'); - const row = db.prepare( - ` + const row = db + .prepare( + ` SELECT ts_ms AS tsMs, typeof(ts_ms) AS tsType, CREATED_DATE AS createdDate FROM imm_session_events WHERE event_id = 1 `, - ).get() as { + ) + .get() as { tsMs: string; tsType: string; createdDate: string; diff --git a/src/core/services/immersion-tracker/storage.ts b/src/core/services/immersion-tracker/storage.ts index f4510f1c..bb4e3e34 100644 --- a/src/core/services/immersion-tracker/storage.ts +++ b/src/core/services/immersion-tracker/storage.ts @@ -171,10 +171,12 @@ function hasColumn(db: DatabaseSync, tableName: string, columnName: string): boo } function getColumnType(db: DatabaseSync, tableName: string, columnName: string): string | null { - const row = (db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ - name: string; - type: string; - }>).find((entry) => entry.name === columnName); + const row = ( + db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ + name: string; + type: string; + }> + ).find((entry) => entry.name === columnName); return row?.type ?? null; } diff --git a/src/core/services/ipc.test.ts b/src/core/services/ipc.test.ts index 847607b6..049cadec 100644 --- a/src/core/services/ipc.test.ts +++ b/src/core/services/ipc.test.ts @@ -886,29 +886,47 @@ test('registerIpcHandlers validates dispatchSessionAction payloads', async () => await dispatchHandler!({}, { actionId: 'unknown-action' }); }, /Invalid session action payload/); - await dispatchHandler!({}, { - actionId: 'copySubtitleMultiple', - payload: { count: 3 }, - }); - await dispatchHandler!({}, { - actionId: 'cycleRuntimeOption', - payload: { - runtimeOptionId: 'anki.autoUpdateNewCards', - direction: -1, + await dispatchHandler!( + {}, + { + actionId: 'copySubtitleMultiple', + payload: { count: 3 }, }, - }); - await dispatchHandler!({}, { - actionId: 'toggleSubtitleSidebar', - }); - await dispatchHandler!({}, { - actionId: 'openSessionHelp', - }); - await dispatchHandler!({}, { - actionId: 'openControllerSelect', - }); - await dispatchHandler!({}, { - actionId: 'openControllerDebug', - }); + ); + await dispatchHandler!( + {}, + { + actionId: 'cycleRuntimeOption', + payload: { + runtimeOptionId: 'anki.autoUpdateNewCards', + direction: -1, + }, + }, + ); + await dispatchHandler!( + {}, + { + actionId: 'toggleSubtitleSidebar', + }, + ); + await dispatchHandler!( + {}, + { + actionId: 'openSessionHelp', + }, + ); + await dispatchHandler!( + {}, + { + actionId: 'openControllerSelect', + }, + ); + await dispatchHandler!( + {}, + { + actionId: 'openControllerDebug', + }, + ); assert.deepEqual(dispatched, [ { diff --git a/src/core/services/overlay-drop.test.ts b/src/core/services/overlay-drop.test.ts index dfc765fa..e3e10522 100644 --- a/src/core/services/overlay-drop.test.ts +++ b/src/core/services/overlay-drop.test.ts @@ -45,11 +45,7 @@ test('collectDroppedVideoPaths parses text/uri-list entries and de-duplicates', test('collectDroppedSubtitlePaths keeps supported dropped subtitle paths in order', () => { const transfer = makeTransfer({ - files: [ - { path: '/subs/ep02.ass' }, - { path: '/subs/readme.txt' }, - { path: '/subs/ep03.SRT' }, - ], + files: [{ path: '/subs/ep02.ass' }, { path: '/subs/readme.txt' }, { path: '/subs/ep03.SRT' }], }); const result = collectDroppedSubtitlePaths(transfer); diff --git a/src/core/services/overlay-visibility.ts b/src/core/services/overlay-visibility.ts index f106783e..768dcc61 100644 --- a/src/core/services/overlay-visibility.ts +++ b/src/core/services/overlay-visibility.ts @@ -158,18 +158,24 @@ export function updateVisibleOverlayVisibility(args: { setOverlayWindowOpacity(mainWindow, 0); mainWindow.showInactive(); mainWindow.setIgnoreMouseEvents(true, { forward: true }); - scheduleWindowsOverlayReveal(mainWindow, shouldBindTrackedWindowsOverlay - ? (window) => args.syncWindowsOverlayToMpvZOrder?.(window) - : undefined); + scheduleWindowsOverlayReveal( + mainWindow, + shouldBindTrackedWindowsOverlay + ? (window) => args.syncWindowsOverlayToMpvZOrder?.(window) + : undefined, + ); } else { if (args.isWindowsPlatform) { setOverlayWindowOpacity(mainWindow, 0); } mainWindow.show(); if (args.isWindowsPlatform) { - scheduleWindowsOverlayReveal(mainWindow, shouldBindTrackedWindowsOverlay - ? (window) => args.syncWindowsOverlayToMpvZOrder?.(window) - : undefined); + scheduleWindowsOverlayReveal( + mainWindow, + shouldBindTrackedWindowsOverlay + ? (window) => args.syncWindowsOverlayToMpvZOrder?.(window) + : undefined, + ); } } } diff --git a/src/core/services/session-bindings.test.ts b/src/core/services/session-bindings.test.ts index 41f07251..cceea9b1 100644 --- a/src/core/services/session-bindings.test.ts +++ b/src/core/services/session-bindings.test.ts @@ -54,9 +54,7 @@ test('compileSessionBindings merges shortcuts and keybindings into one canonical code: binding.key.code, modifiers: binding.key.modifiers, target: - binding.actionType === 'session-action' - ? binding.actionId - : binding.command.join(' '), + binding.actionType === 'session-action' ? binding.actionId : binding.command.join(' '), })), [ { @@ -191,9 +189,10 @@ test('compileSessionBindings omits disabled bindings', () => { }); assert.equal(result.warnings.length, 0); - assert.deepEqual(result.bindings.map((binding) => binding.sourcePath), [ - 'shortcuts.toggleVisibleOverlayGlobal', - ]); + assert.deepEqual( + result.bindings.map((binding) => binding.sourcePath), + ['shortcuts.toggleVisibleOverlayGlobal'], + ); }); test('compileSessionBindings warns on unsupported shortcut and keybinding syntax', () => { @@ -222,12 +221,16 @@ test('compileSessionBindings rejects malformed command arrays', () => { platform: 'linux', }); - assert.deepEqual(result.bindings.map((binding) => binding.sourcePath), ['keybindings[0].key']); + assert.deepEqual( + result.bindings.map((binding) => binding.sourcePath), + ['keybindings[0].key'], + ); assert.equal(result.bindings[0]?.actionType, 'mpv-command'); assert.deepEqual(result.bindings[0]?.command, ['show-text', 3000]); - assert.deepEqual(result.warnings.map((warning) => `${warning.kind}:${warning.path}`), [ - 'unsupported:keybindings[1].command', - ]); + assert.deepEqual( + result.warnings.map((warning) => `${warning.kind}:${warning.path}`), + ['unsupported:keybindings[1].command'], + ); }); test('compileSessionBindings rejects non-string command heads and extra args on special commands', () => { @@ -241,10 +244,10 @@ test('compileSessionBindings rejects non-string command heads and extra args on }); assert.deepEqual(result.bindings, []); - assert.deepEqual(result.warnings.map((warning) => `${warning.kind}:${warning.path}`), [ - 'unsupported:keybindings[0].command', - 'unsupported:keybindings[1].command', - ]); + assert.deepEqual( + result.warnings.map((warning) => `${warning.kind}:${warning.path}`), + ['unsupported:keybindings[0].command', 'unsupported:keybindings[1].command'], + ); }); test('compileSessionBindings points unsupported command warnings at the command field', () => { @@ -255,9 +258,10 @@ test('compileSessionBindings points unsupported command warnings at the command }); assert.deepEqual(result.bindings, []); - assert.deepEqual(result.warnings.map((warning) => `${warning.kind}:${warning.path}`), [ - 'unsupported:keybindings[0].command', - ]); + assert.deepEqual( + result.warnings.map((warning) => `${warning.kind}:${warning.path}`), + ['unsupported:keybindings[0].command'], + ); }); test('compileSessionBindings warns on deprecated toggleVisibleOverlayGlobal config', () => { diff --git a/src/core/services/session-bindings.ts b/src/core/services/session-bindings.ts index 7b8272f4..d3956407 100644 --- a/src/core/services/session-bindings.ts +++ b/src/core/services/session-bindings.ts @@ -342,9 +342,7 @@ function getBindingFingerprint(binding: CompiledSessionBinding): string { return `session:${binding.actionId}:${JSON.stringify(binding.payload ?? null)}`; } -export function compileSessionBindings( - input: CompileSessionBindingsInput, -): { +export function compileSessionBindings(input: CompileSessionBindingsInput): { bindings: CompiledSessionBinding[]; warnings: SessionBindingWarning[]; } { diff --git a/src/main.ts b/src/main.ts index 377444d2..dc45d5c3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -415,7 +415,10 @@ import { createAnilistRateLimiter } from './core/services/anilist/rate-limiter'; import { createJellyfinTokenStore } from './core/services/jellyfin-token-store'; import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc'; import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store'; -import { buildPluginSessionBindingsArtifact, compileSessionBindings } from './core/services/session-bindings'; +import { + buildPluginSessionBindingsArtifact, + compileSessionBindings, +} from './core/services/session-bindings'; import { dispatchSessionAction as dispatchSessionActionCore } from './core/services/session-actions'; import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts'; import { createMainRuntimeRegistry } from './main/runtime/registry'; @@ -1933,9 +1936,7 @@ function getWindowsNativeWindowHandle(window: BrowserWindow): string { function getWindowsNativeWindowHandleNumber(window: BrowserWindow): number { const handle = window.getNativeWindowHandle(); - return handle.length >= 8 - ? Number(handle.readBigUInt64LE(0)) - : handle.readUInt32LE(0); + return handle.length >= 8 ? Number(handle.readBigUInt64LE(0)) : handle.readUInt32LE(0); } function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | null): number | null { @@ -1945,11 +1946,9 @@ function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | nu try { if (targetMpvSocketPath) { - const windowTracker = appState.windowTracker as - | { - getTargetWindowHandle?: () => number | null; - } - | null; + const windowTracker = appState.windowTracker as { + getTargetWindowHandle?: () => number | null; + } | null; const trackedHandle = windowTracker?.getTargetWindowHandle?.(); if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) { return trackedHandle; @@ -2255,14 +2254,16 @@ function openOverlayHostedModalWithOsd( unavailableMessage: string, failureLogMessage: string, ): void { - void openModal(createOverlayHostedModalOpenDeps()).then((opened) => { - if (!opened) { + void openModal(createOverlayHostedModalOpenDeps()) + .then((opened) => { + if (!opened) { + showMpvOsd(unavailableMessage); + } + }) + .catch((error) => { + logger.error(failureLogMessage, error); showMpvOsd(unavailableMessage); - } - }).catch((error) => { - logger.error(failureLogMessage, error); - showMpvOsd(unavailableMessage); - }); + }); } function openRuntimeOptionsPalette(): void { @@ -4932,7 +4933,8 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({ printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT), stopApp: () => requestAppQuit(), hasMainWindow: () => Boolean(overlayManager.getMainWindow()), - dispatchSessionAction: (request: SessionActionDispatchRequest) => dispatchSessionAction(request), + dispatchSessionAction: (request: SessionActionDispatchRequest) => + dispatchSessionAction(request), getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs, schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs), logInfo: (message: string) => logger.info(message), @@ -5200,14 +5202,18 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } = if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return; const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow); const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(appState.mpvSocketPath); - if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) { + if ( + targetWindowHwnd !== null && + bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd) + ) { return; } const tracker = appState.windowTracker; const mpvResult = tracker ? (() => { try { - const win32 = require('./window-trackers/win32') as typeof import('./window-trackers/win32'); + const win32 = + require('./window-trackers/win32') as typeof import('./window-trackers/win32'); const poll = win32.findMpvWindows(); const focused = poll.matches.find((m) => m.isForeground); return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null; diff --git a/src/main/boot/services.ts b/src/main/boot/services.ts index f4f46582..24afbf5f 100644 --- a/src/main/boot/services.ts +++ b/src/main/boot/services.ts @@ -132,7 +132,10 @@ export function createMainBootServices< TSubtitleWebSocket, TLogger, TRuntimeRegistry, - TOverlayManager extends { getMainWindow: () => BrowserWindow | null; getModalWindow: () => BrowserWindow | null }, + TOverlayManager extends { + getMainWindow: () => BrowserWindow | null; + getModalWindow: () => BrowserWindow | null; + }, TOverlayModalInputState extends OverlayModalInputStateShape, TOverlayContentMeasurementStore, TOverlayModalRuntime, diff --git a/src/main/overlay-runtime.test.ts b/src/main/overlay-runtime.test.ts index a3061009..a9a80035 100644 --- a/src/main/overlay-runtime.test.ts +++ b/src/main/overlay-runtime.test.ts @@ -180,13 +180,15 @@ function createMockWindow(): MockWindow & { get: () => state.contentReady, set: (value: boolean) => { state.contentReady = value; - (window as typeof window & { __subminerOverlayContentReady?: boolean }).__subminerOverlayContentReady = - value; + ( + window as typeof window & { __subminerOverlayContentReady?: boolean } + ).__subminerOverlayContentReady = value; }, }); - (window as typeof window & { __subminerOverlayContentReady?: boolean }).__subminerOverlayContentReady = - state.contentReady; + ( + window as typeof window & { __subminerOverlayContentReady?: boolean } + ).__subminerOverlayContentReady = state.contentReady; return window; } @@ -561,23 +563,26 @@ test('handleOverlayModalClosed destroys modal window for single kiku modal', () test('modal fallback reveal skips showing window when content is not ready', async () => { const window = createMockWindow(); let scheduledReveal: (() => void) | null = null; - const runtime = createOverlayModalRuntimeService({ - getMainWindow: () => null, - getModalWindow: () => window as never, - createModalWindow: () => { - throw new Error('modal window should not be created when already present'); + const runtime = createOverlayModalRuntimeService( + { + getMainWindow: () => null, + getModalWindow: () => window as never, + createModalWindow: () => { + throw new Error('modal window should not be created when already present'); + }, + getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), + setModalWindowBounds: () => {}, }, - getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), - setModalWindowBounds: () => {}, - }, { - scheduleRevealFallback: (callback) => { - scheduledReveal = callback; - return { scheduled: true } as never; + { + scheduleRevealFallback: (callback) => { + scheduledReveal = callback; + return { scheduled: true } as never; + }, + clearRevealFallback: () => { + scheduledReveal = null; + }, }, - clearRevealFallback: () => { - scheduledReveal = null; - }, - }); + ); window.loading = true; window.url = ''; diff --git a/src/main/overlay-runtime.ts b/src/main/overlay-runtime.ts index 242e54ea..0bb60978 100644 --- a/src/main/overlay-runtime.ts +++ b/src/main/overlay-runtime.ts @@ -54,10 +54,7 @@ type RevealFallbackHandle = NonNullable void; - scheduleRevealFallback?: ( - callback: () => void, - delayMs: number, - ) => RevealFallbackHandle; + scheduleRevealFallback?: (callback: () => void, delayMs: number) => RevealFallbackHandle; clearRevealFallback?: (timeout: RevealFallbackHandle) => void; } @@ -73,10 +70,7 @@ export function createOverlayModalRuntimeService( let modalWindowPrimedForImmediateShow = false; let pendingModalWindowReveal: BrowserWindow | null = null; let pendingModalWindowRevealTimeout: RevealFallbackHandle | null = null; - const scheduleRevealFallback = ( - callback: () => void, - delayMs: number, - ): RevealFallbackHandle => + const scheduleRevealFallback = (callback: () => void, delayMs: number): RevealFallbackHandle => (options.scheduleRevealFallback ?? globalThis.setTimeout)(callback, delayMs); const clearRevealFallback = (timeout: RevealFallbackHandle): void => (options.clearRevealFallback ?? globalThis.clearTimeout)(timeout); diff --git a/src/main/runtime/app-lifecycle-actions.test.ts b/src/main/runtime/app-lifecycle-actions.test.ts index 5fa4fd0e..8d5727bd 100644 --- a/src/main/runtime/app-lifecycle-actions.test.ts +++ b/src/main/runtime/app-lifecycle-actions.test.ts @@ -16,7 +16,8 @@ test('on will quit cleanup handler runs all cleanup steps', () => { unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'), stopSubtitleWebsocket: () => calls.push('stop-ws'), stopTexthookerService: () => calls.push('stop-texthooker'), - clearWindowsVisibleOverlayForegroundPollLoop: () => calls.push('clear-windows-visible-overlay-poll'), + clearWindowsVisibleOverlayForegroundPollLoop: () => + calls.push('clear-windows-visible-overlay-poll'), destroyMainOverlayWindow: () => calls.push('destroy-main-overlay-window'), destroyModalOverlayWindow: () => calls.push('destroy-modal-overlay-window'), destroyYomitanParserWindow: () => calls.push('destroy-yomitan-window'), diff --git a/src/main/runtime/config-hot-reload-handlers.ts b/src/main/runtime/config-hot-reload-handlers.ts index 0f752943..89f53977 100644 --- a/src/main/runtime/config-hot-reload-handlers.ts +++ b/src/main/runtime/config-hot-reload-handlers.ts @@ -45,11 +45,7 @@ export function buildConfigHotReloadPayload(config: ResolvedConfig): ConfigHotRe shortcuts: resolveConfiguredShortcuts(config, DEFAULT_CONFIG), statsToggleKey: config.stats.toggleKey, platform: - process.platform === 'darwin' - ? 'darwin' - : process.platform === 'win32' - ? 'win32' - : 'linux', + process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'win32' : 'linux', rawConfig: config, }); return { diff --git a/src/main/runtime/first-run-setup-service.test.ts b/src/main/runtime/first-run-setup-service.test.ts index 4111024b..80ca0557 100644 --- a/src/main/runtime/first-run-setup-service.test.ts +++ b/src/main/runtime/first-run-setup-service.test.ts @@ -94,10 +94,7 @@ test('shouldAutoOpenFirstRunSetup only for startup/setup intents', () => { }); test('shouldAutoOpenFirstRunSetup treats numeric startup counts as explicit commands', () => { - assert.equal( - shouldAutoOpenFirstRunSetup(makeArgs({ start: true, copySubtitleCount: 2 })), - false, - ); + assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ start: true, copySubtitleCount: 2 })), false); assert.equal( shouldAutoOpenFirstRunSetup(makeArgs({ background: true, mineSentenceCount: 1 })), false, diff --git a/src/main/runtime/immersion-startup.test.ts b/src/main/runtime/immersion-startup.test.ts index 2388c829..79917202 100644 --- a/src/main/runtime/immersion-startup.test.ts +++ b/src/main/runtime/immersion-startup.test.ts @@ -194,7 +194,9 @@ test('createImmersionTrackerStartupHandler keeps tracker startup alive when mpv ), ); assert.equal( - calls.some((entry) => entry.startsWith('warn:Immersion tracker startup failed; disabling tracking.')), + calls.some((entry) => + entry.startsWith('warn:Immersion tracker startup failed; disabling tracking.'), + ), false, ); }); diff --git a/src/main/runtime/immersion-startup.ts b/src/main/runtime/immersion-startup.ts index bf533d2d..142eb23f 100644 --- a/src/main/runtime/immersion-startup.ts +++ b/src/main/runtime/immersion-startup.ts @@ -105,7 +105,10 @@ export function createImmersionTrackerStartupHandler( try { mpvClient.connect(); } catch (error) { - deps.logWarn('MPV auto-connect failed during immersion tracker startup; continuing.', error); + deps.logWarn( + 'MPV auto-connect failed during immersion tracker startup; continuing.', + error, + ); } } deps.seedTrackerFromCurrentMedia(); diff --git a/src/preload.ts b/src/preload.ts index 7bb846bb..b29be004 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -124,7 +124,9 @@ function createQueuedIpcListenerWithPayload( const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen); const onOpenSessionHelpEvent = createQueuedIpcListener(IPC_CHANNELS.event.sessionHelpOpen); -const onOpenControllerSelectEvent = createQueuedIpcListener(IPC_CHANNELS.event.controllerSelectOpen); +const onOpenControllerSelectEvent = createQueuedIpcListener( + IPC_CHANNELS.event.controllerSelectOpen, +); const onOpenControllerDebugEvent = createQueuedIpcListener(IPC_CHANNELS.event.controllerDebugOpen); const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen); const onOpenYoutubeTrackPickerEvent = createQueuedIpcListenerWithPayload( diff --git a/src/prerelease-workflow.test.ts b/src/prerelease-workflow.test.ts index 734c04bb..be0f3b74 100644 --- a/src/prerelease-workflow.test.ts +++ b/src/prerelease-workflow.test.ts @@ -80,7 +80,10 @@ test('prerelease workflow writes checksum entries using release asset basenames' assert.match(prereleaseWorkflow, /: > release\/SHA256SUMS\.txt/); assert.match(prereleaseWorkflow, /for file in "\$\{files\[@\]\}"; do/); assert.match(prereleaseWorkflow, /\$\{file##\*\/\}/); - assert.doesNotMatch(prereleaseWorkflow, /sha256sum "\$\{files\[@\]\}" > release\/SHA256SUMS\.txt/); + assert.doesNotMatch( + prereleaseWorkflow, + /sha256sum "\$\{files\[@\]\}" > release\/SHA256SUMS\.txt/, + ); }); test('prerelease workflow validates artifacts before publishing the release and only undrafts after upload', () => { diff --git a/src/renderer/error-recovery.test.ts b/src/renderer/error-recovery.test.ts index dabd10e0..99a7e553 100644 --- a/src/renderer/error-recovery.test.ts +++ b/src/renderer/error-recovery.test.ts @@ -364,7 +364,10 @@ test('isYomitanPopupVisible requires visible iframe geometry', () => { const root = { querySelectorAll: (value: string) => { selectors.push(value); - if (value === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR || value === YOMITAN_POPUP_HOST_SELECTOR) { + if ( + value === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR || + value === YOMITAN_POPUP_HOST_SELECTOR + ) { return []; } return [hiddenFrame, visibleFrame]; diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index 63999d80..3d6caa0e 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -1063,7 +1063,9 @@ test('session binding: Ctrl+Alt+S dispatches subsync action locally', async () = testGlobals.dispatchKeydown({ key: 's', code: 'KeyS', ctrlKey: true, altKey: true }); - assert.deepEqual(testGlobals.sessionActions, [{ actionId: 'triggerSubsync', payload: undefined }]); + assert.deepEqual(testGlobals.sessionActions, [ + { actionId: 'triggerSubsync', payload: undefined }, + ]); } finally { testGlobals.restore(); } diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index ca99e31c..33a617a4 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -39,12 +39,10 @@ export function createKeyboardHandlers( let pendingLookupRefreshAfterSubtitleSeek = false; let resetSelectionToStartOnNextSubtitleSync = false; let lookupScanFallbackTimer: ReturnType | null = null; - let pendingNumericSelection: - | { - actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple'; - timeout: ReturnType | null; - } - | null = null; + let pendingNumericSelection: { + actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple'; + timeout: ReturnType | null; + } | null = null; const CHORD_MAP = new Map< string, diff --git a/src/renderer/handlers/mouse.ts b/src/renderer/handlers/mouse.ts index 5180eb73..7d193be9 100644 --- a/src/renderer/handlers/mouse.ts +++ b/src/renderer/handlers/mouse.ts @@ -73,11 +73,13 @@ export function createMouseHandlers( syncOverlayMouseIgnoreState(ctx); } - function reconcilePopupInteraction(args: { - assumeVisible?: boolean; - reclaimFocus?: boolean; - allowPause?: boolean; - } = {}): boolean { + function reconcilePopupInteraction( + args: { + assumeVisible?: boolean; + reclaimFocus?: boolean; + allowPause?: boolean; + } = {}, + ): boolean { const popupVisible = syncPopupVisibilityState(args.assumeVisible === true); if (!popupVisible) { syncOverlayMouseIgnoreState(ctx); diff --git a/src/renderer/modals/runtime-options.test.ts b/src/renderer/modals/runtime-options.test.ts index a1d3a5de..9fc5219c 100644 --- a/src/renderer/modals/runtime-options.test.ts +++ b/src/renderer/modals/runtime-options.test.ts @@ -168,48 +168,54 @@ function withRuntimeOptionsModal( test('openRuntimeOptionsModal shows loading shell before runtime options resolve', async () => { const deferred = createDeferred(); - await withRuntimeOptionsModal(() => deferred.promise, async (input) => { - input.modal.openRuntimeOptionsModal(); + await withRuntimeOptionsModal( + () => deferred.promise, + async (input) => { + input.modal.openRuntimeOptionsModal(); - assert.equal(input.state.runtimeOptionsModalOpen, true); - assert.equal(input.overlayClassList.contains('interactive'), true); - assert.equal(input.modalClassList.contains('hidden'), false); - assert.equal(input.statusNode.textContent, 'Loading runtime options...'); - assert.deepEqual(input.syncCalls, ['sync']); + assert.equal(input.state.runtimeOptionsModalOpen, true); + assert.equal(input.overlayClassList.contains('interactive'), true); + assert.equal(input.modalClassList.contains('hidden'), false); + assert.equal(input.statusNode.textContent, 'Loading runtime options...'); + assert.deepEqual(input.syncCalls, ['sync']); - deferred.resolve([ - { - id: 'anki.autoUpdateNewCards', - label: 'Auto-update new cards', - scope: 'ankiConnect', - valueType: 'boolean', - value: true, - allowedValues: [true, false], - requiresRestart: false, - }, - ]); - await flushAsyncWork(); + deferred.resolve([ + { + id: 'anki.autoUpdateNewCards', + label: 'Auto-update new cards', + scope: 'ankiConnect', + valueType: 'boolean', + value: true, + allowedValues: [true, false], + requiresRestart: false, + }, + ]); + await flushAsyncWork(); - assert.equal( - input.statusNode.textContent, - 'Use arrow keys. Click value to cycle. Enter or double-click to apply.', - ); - assert.equal(input.statusNode.classList.contains('error'), false); - }); + assert.equal( + input.statusNode.textContent, + 'Use arrow keys. Click value to cycle. Enter or double-click to apply.', + ); + assert.equal(input.statusNode.classList.contains('error'), false); + }, + ); }); test('openRuntimeOptionsModal keeps modal visible when loading fails', async () => { const deferred = createDeferred(); - await withRuntimeOptionsModal(() => deferred.promise, async (input) => { - input.modal.openRuntimeOptionsModal(); - deferred.reject(new Error('boom')); - await flushAsyncWork(); + await withRuntimeOptionsModal( + () => deferred.promise, + async (input) => { + input.modal.openRuntimeOptionsModal(); + deferred.reject(new Error('boom')); + await flushAsyncWork(); - assert.equal(input.state.runtimeOptionsModalOpen, true); - assert.equal(input.overlayClassList.contains('interactive'), true); - assert.equal(input.modalClassList.contains('hidden'), false); - assert.equal(input.statusNode.textContent, 'Failed to load runtime options'); - assert.equal(input.statusNode.classList.contains('error'), true); - }); + assert.equal(input.state.runtimeOptionsModalOpen, true); + assert.equal(input.overlayClassList.contains('interactive'), true); + assert.equal(input.modalClassList.contains('hidden'), false); + assert.equal(input.statusNode.textContent, 'Failed to load runtime options'); + assert.equal(input.statusNode.classList.contains('error'), true); + }, + ); }); diff --git a/src/renderer/overlay-mouse-ignore.test.ts b/src/renderer/overlay-mouse-ignore.test.ts index 2c6e1617..ee85db27 100644 --- a/src/renderer/overlay-mouse-ignore.test.ts +++ b/src/renderer/overlay-mouse-ignore.test.ts @@ -130,7 +130,8 @@ test('visible yomitan popup host keeps overlay interactive even when cached popu }, document: { querySelectorAll: (selector: string) => - selector === '[data-subminer-yomitan-popup-host="true"][data-subminer-yomitan-popup-visible="true"]' + selector === + '[data-subminer-yomitan-popup-host="true"][data-subminer-yomitan-popup-visible="true"]' ? [{ getAttribute: () => 'true' }] : [], }, diff --git a/src/renderer/yomitan-popup.ts b/src/renderer/yomitan-popup.ts index 6aeb0c93..2d8006b6 100644 --- a/src/renderer/yomitan-popup.ts +++ b/src/renderer/yomitan-popup.ts @@ -73,7 +73,10 @@ function queryPopupElements( } export function isYomitanPopupVisible(root: ParentNode | null | undefined = document): boolean { - const visiblePopupHosts = queryPopupElements(root, YOMITAN_POPUP_VISIBLE_HOST_SELECTOR); + const visiblePopupHosts = queryPopupElements( + root, + YOMITAN_POPUP_VISIBLE_HOST_SELECTOR, + ); if (visiblePopupHosts.length > 0) { return true; } diff --git a/src/shared/ipc/validators.ts b/src/shared/ipc/validators.ts index 4fcb8c57..a7939f67 100644 --- a/src/shared/ipc/validators.ts +++ b/src/shared/ipc/validators.ts @@ -256,7 +256,9 @@ export function parseSessionActionDispatchRequest( const payload = parseSessionActionPayload(value.actionId, value.payload); if (payload === null) return null; - return payload === undefined ? { actionId: value.actionId } : { actionId: value.actionId, payload }; + return payload === undefined + ? { actionId: value.actionId } + : { actionId: value.actionId, payload }; } export function parseMpvCommand(value: unknown): Array | null { diff --git a/src/types/runtime.ts b/src/types/runtime.ts index fa8f60a6..f68f1a8a 100644 --- a/src/types/runtime.ts +++ b/src/types/runtime.ts @@ -364,7 +364,10 @@ export interface ElectronAPI { getKeybindings: () => Promise; getSessionBindings: () => Promise; getConfiguredShortcuts: () => Promise>; - dispatchSessionAction: (actionId: SessionActionId, payload?: SessionActionPayload) => Promise; + dispatchSessionAction: ( + actionId: SessionActionId, + payload?: SessionActionPayload, + ) => Promise; getStatsToggleKey: () => Promise; getMarkWatchedKey: () => Promise; markActiveVideoWatched: () => Promise; diff --git a/src/types/session-bindings.ts b/src/types/session-bindings.ts index 9b510bc8..16b32e15 100644 --- a/src/types/session-bindings.ts +++ b/src/types/session-bindings.ts @@ -62,9 +62,7 @@ export interface CompiledSessionActionBinding extends CompiledSessionBindingBase payload?: SessionActionPayload; } -export type CompiledSessionBinding = - | CompiledMpvCommandBinding - | CompiledSessionActionBinding; +export type CompiledSessionBinding = CompiledMpvCommandBinding | CompiledSessionActionBinding; export interface PluginSessionBindingsArtifact { version: 1; diff --git a/src/window-trackers/mpv-socket-match.test.ts b/src/window-trackers/mpv-socket-match.test.ts index bec350d8..e35e5eb6 100644 --- a/src/window-trackers/mpv-socket-match.test.ts +++ b/src/window-trackers/mpv-socket-match.test.ts @@ -1,6 +1,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { filterMpvPollResultBySocketPath, matchesMpvSocketPathInCommandLine } from './mpv-socket-match'; +import { + filterMpvPollResultBySocketPath, + matchesMpvSocketPathInCommandLine, +} from './mpv-socket-match'; import type { MpvPollResult } from './win32'; function createPollResult(commandLines: Array): MpvPollResult { @@ -51,7 +54,10 @@ test('filterMpvPollResultBySocketPath keeps only matches for the requested socke '\\\\.\\pipe\\subminer-b', ); - assert.deepEqual(result.matches.map((match) => match.hwnd), [2]); + assert.deepEqual( + result.matches.map((match) => match.hwnd), + [2], + ); assert.equal(result.windowState, 'visible'); }); diff --git a/src/window-trackers/windows-helper.ts b/src/window-trackers/windows-helper.ts index c71787ff..5a9ef40f 100644 --- a/src/window-trackers/windows-helper.ts +++ b/src/window-trackers/windows-helper.ts @@ -26,7 +26,8 @@ export function findWindowsMpvTargetWindowHandle(result?: MpvPollResult): number const poll = result ?? loadWin32().findMpvWindows(); const focused = poll.matches.find((match) => match.isForeground); const best = - focused ?? [...poll.matches].sort((a, b) => b.area - a.area || b.bounds.width - a.bounds.width)[0]; + focused ?? + [...poll.matches].sort((a, b) => b.area - a.area || b.bounds.width - a.bounds.width)[0]; return best?.hwnd ?? null; } diff --git a/src/window-trackers/windows-tracker.test.ts b/src/window-trackers/windows-tracker.test.ts index da353a27..24afefb0 100644 --- a/src/window-trackers/windows-tracker.test.ts +++ b/src/window-trackers/windows-tracker.test.ts @@ -4,7 +4,9 @@ import { WindowsWindowTracker } from './windows-tracker'; import type { MpvPollResult } from './win32'; function mpvVisible( - overrides: Partial = {}, + overrides: Partial< + MpvPollResult & { x?: number; y?: number; width?: number; height?: number; focused?: boolean } + > = {}, ): MpvPollResult { return { matches: [ diff --git a/src/window-trackers/windows-tracker.ts b/src/window-trackers/windows-tracker.ts index 4152aaa6..5b695c03 100644 --- a/src/window-trackers/windows-tracker.ts +++ b/src/window-trackers/windows-tracker.ts @@ -55,7 +55,8 @@ export class WindowsWindowTracker extends BaseWindowTracker { constructor(_targetMpvSocketPath?: string, deps: WindowsTrackerDeps = {}) { super(); this.targetMpvSocketPath = _targetMpvSocketPath?.trim() || null; - this.pollMpvWindows = deps.pollMpvWindows ?? (() => defaultPollMpvWindows(this.targetMpvSocketPath)); + this.pollMpvWindows = + deps.pollMpvWindows ?? (() => defaultPollMpvWindows(this.targetMpvSocketPath)); this.maxConsecutiveMisses = Math.max(1, Math.floor(deps.maxConsecutiveMisses ?? 2)); this.trackingLossGraceMs = Math.max(0, Math.floor(deps.trackingLossGraceMs ?? 1_500)); this.minimizedTrackingLossGraceMs = Math.max(