Compare commits

..

28 Commits

Author SHA1 Message Date
sudacode 9ce5de2f22 fix: address PR #49 CodeRabbit review comments 2026-04-11 15:49:18 -07:00
sudacode 95f858292e chore: add backlog note for rebase fix 2026-04-11 15:09:57 -07:00
sudacode d16ed7d879 fix: address PR 49 CodeRabbit follow-ups 2026-04-11 15:08:26 -07:00
sudacode 953f4c362b fix: address remaining CodeRabbit PR feedback 2026-04-11 15:08:26 -07:00
sudacode f7fbffd4f5 fix: restore controller modal shortcut opens 2026-04-11 15:08:26 -07:00
sudacode ef41121774 fix: address PR #49 CodeRabbit follow-ups 2026-04-11 15:08:26 -07:00
sudacode 5b4844c16a chore(release): v0.12.0-beta.3 2026-04-11 15:08:26 -07:00
sudacode 3c711ed00b Honor configured controller shortcuts and clean up modal opens 2026-04-11 15:08:26 -07:00
sudacode 1d4b3b4ed0 refactor: simplify Windows tracker helper API 2026-04-11 15:08:26 -07:00
sudacode ad33ec57c6 fix: address latest CodeRabbit review 2026-04-11 15:08:26 -07:00
sudacode 26cb4704f1 fix: address CodeRabbit review and ci 2026-04-11 15:08:26 -07:00
sudacode 18940b57c0 fix: address CodeRabbit review round 3 2026-04-11 15:08:26 -07:00
sudacode 735fc26525 fix: address CodeRabbit follow-ups 2026-04-11 15:08:26 -07:00
sudacode 5711e1cb49 Address second CodeRabbit review round 2026-04-11 15:08:26 -07:00
sudacode fd05b3f868 fix(session-bindings): guard optional stats config in compile path 2026-04-11 15:08:26 -07:00
sudacode dd7896ca83 feat: wire session bindings through main, ipc, and cli runtime 2026-04-11 15:08:26 -07:00
sudacode 7b828ee9d1 fix(ci): sync bun.lock for bun 1.3.5 frozen install 2026-04-11 15:08:26 -07:00
sudacode 888b9a7ac4 Fix Windows CodeRabbit review follow-ups 2026-04-11 15:08:26 -07:00
sudacode 59f30effc4 Fix Windows overlay z-order on minimize/restore and improve hover stability
Use native synchronous z-order binding (koffi) instead of async PowerShell
for overlay positioning, eliminating the 200-500ms delay that left the overlay
behind mpv after restore. Hide the overlay immediately when mpv is minimized
so the full show/reveal/z-order flow triggers cleanly on restore.

Also adds hover suppression after visibility recovery and window resize to
prevent spurious auto-pause, Windows secondary subtitle titlebar fix, and
z-order sync burst retries on geometry changes.
2026-04-11 15:08:26 -07:00
sudacode 19e210c3a0 Fix Windows secondary hover titlebar blocking 2026-04-11 15:08:26 -07:00
sudacode f457801708 Fix Windows overlay tracking, z-order, and startup visibility
- switch Windows overlay tracking to native win32 polling with native owner and z-order helpers
- keep the visible overlay and stats overlay aligned across focus handoff, transient tracker misses, and minimize/restore cycles
- start the visible overlay click-through and hide the initial opaque startup frame until the tracked transparent state settles
- add a backlog task for the inconsistent mpv y-t overlay toggle after menu toggles
2026-04-11 15:08:26 -07:00
sudacode fff54e914a fix: exclude prerelease tags from stable workflow 2026-04-11 15:08:26 -07:00
sudacode 09425e8e67 chore: prep 0.12.0-beta.1 prerelease workflow 2026-04-11 15:08:26 -07:00
sudacode 1ad29f0625 Fix Yomitan blur guard 2026-04-11 15:08:26 -07:00
sudacode ba3503330c Reconcile Yomitan observer on setup 2026-04-11 15:08:26 -07:00
sudacode f17ff006b4 Fix overlay subtitle drop routing 2026-04-11 15:08:26 -07:00
sudacode ecfa113886 Keep overlay interactive while Yomitan popup is visible 2026-04-11 15:08:26 -07:00
sudacode 47b26cc4a5 Fix nested Yomitan popup focus loss 2026-04-11 15:08:26 -07:00
64 changed files with 329 additions and 743 deletions
+25 -40
View File
@@ -32,9 +32,9 @@ jobs:
node_modules
stats/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
${{ runner.os }}-bun-
- name: Install dependencies
run: |
@@ -50,9 +50,6 @@ jobs:
- name: Test suite (source)
run: bun run test:fast
- name: Environment suite
run: bun run test:env
- name: Coverage suite (maintained source lane)
run: bun run test:coverage:src
@@ -106,9 +103,9 @@ jobs:
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
${{ runner.os }}-bun-
- name: Install dependencies
run: |
@@ -165,9 +162,9 @@ jobs:
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
${{ runner.os }}-bun-
- name: Validate macOS signing/notarization secrets
run: |
@@ -241,9 +238,9 @@ jobs:
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
${{ runner.os }}-bun-
- name: Install dependencies
run: |
@@ -309,9 +306,9 @@ jobs:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
@@ -344,12 +341,7 @@ jobs:
echo "No release artifacts found for checksum generation."
exit 1
fi
: > release/SHA256SUMS.txt
for file in "${files[@]}"; do
printf '%s %s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"${file##*/}" >> release/SHA256SUMS.txt
done
sha256sum "${files[@]}" > release/SHA256SUMS.txt
- name: Get version from tag
id: version
@@ -364,6 +356,20 @@ jobs:
run: |
set -euo pipefail
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then
gh release edit "${{ steps.version.outputs.VERSION }}" \
--draft=false \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
else
gh release create "${{ steps.version.outputs.VERSION }}" \
--latest=false \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
fi
shopt -s nullglob
artifacts=(
release/*.AppImage
@@ -380,27 +386,6 @@ jobs:
exit 1
fi
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then
gh release edit "${{ steps.version.outputs.VERSION }}" \
--draft \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
else
gh release create "${{ steps.version.outputs.VERSION }}" \
--draft \
--latest=false \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
fi
for asset in "${artifacts[@]}"; do
gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber
done
gh release edit "${{ steps.version.outputs.VERSION }}" \
--draft=false \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
+1 -6
View File
@@ -340,12 +340,7 @@ jobs:
echo "No release artifacts found for checksum generation."
exit 1
fi
: > release/SHA256SUMS.txt
for file in "${files[@]}"; do
printf '%s %s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"${file##*/}" >> release/SHA256SUMS.txt
done
sha256sum "${files[@]}" > release/SHA256SUMS.txt
- name: Get version from tag
id: version
-38
View File
@@ -1,43 +1,5 @@
# 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
@@ -1,61 +0,0 @@
---
id: TASK-286.1
title: 'Assess and address PR #49 subsequent CodeRabbit review round'
status: Done
assignee: []
created_date: '2026-04-11 23:14'
updated_date: '2026-04-11 23:16'
labels:
- bug
- code-review
- windows
- release
dependencies: []
references:
- .github/workflows/prerelease.yml
- src/window-trackers/mpv-socket-match.ts
- src/window-trackers/win32.ts
- src/core/services/overlay-shortcut.ts
parent_task_id: TASK-286
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Track the next unresolved CodeRabbit review threads on PR #49 after commit 9ce5de2f and resolve the still-valid follow-up issues without reopening already-assessed stale comments.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All still-actionable CodeRabbit comments in the latest PR #49 round are fixed or explicitly shown stale with evidence.
- [x] #2 Regression coverage is added or updated for any behavior-sensitive fix in workflow or Windows socket matching.
- [x] #3 Relevant verification passes for the touched workflow, tracker, and shared matcher changes.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Verify the five unresolved CodeRabbit threads against current branch state and separate still-valid bugs from stale comments.
2. Add or extend the narrowest failing tests for exact socket-path matching and prerelease workflow invariants before changing production code.
3. Implement minimal fixes in the prerelease workflow and Windows socket matching/cache path, leaving stale comments documented with evidence instead of forcing no-op changes.
4. Run targeted verification, then record the fixed-vs-stale assessment and close the subtask.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Assessed five unresolved PR #49 threads after 9ce5de2f. Fixed prerelease workflow cache keys to include `runner.arch`, changed prerelease publishing to validate artifacts before release creation/edit and only undraft after uploads complete, tightened Windows socket matching to require exact argument boundaries, and stopped memoizing null command-line lookup misses in the Win32 cache path.
Stale assessment: the `src/core/services/overlay-shortcut.ts` thread is still obsolete. Current code at `registerOverlayShortcuts()` returns `hasConfiguredOverlayShortcuts(shortcuts)`, not `false`, and the overlay-local handling remains intentionally driven by local fallback dispatch rather than global registration in this runtime path.
Verification: `bun test src/prerelease-workflow.test.ts src/window-trackers/mpv-socket-match.test.ts`, `bun test src/window-trackers/windows-tracker.test.ts src/prerelease-workflow.test.ts src/window-trackers/mpv-socket-match.test.ts`, `bun run typecheck`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Handled the next CodeRabbit round on PR #49 by fixing the still-valid prerelease workflow and Windows socket-matching issues while documenting the stale overlay-shortcut comment instead of forcing a no-op code change. The prerelease workflow now scopes all dependency caches by `runner.arch`, validates the final artifact set before touching the GitHub release, creates/edits the prerelease as a draft during uploads, and only flips `--draft=false` after all assets succeed. On Windows, socket matching now requires an exact `--input-ipc-server` argument boundary so `subminer-1` no longer matches `subminer-10`, and transient PowerShell/CIM misses no longer get cached forever as null command lines.
Regression coverage was added for the workflow invariants and exact socket matching. Verification passed with targeted prerelease workflow tests, Windows tracker tests, socket-matcher tests, and `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,49 +0,0 @@
---
id: TASK-286.2
title: 'Assess and address PR #49 next CodeRabbit review round'
status: Done
assignee: []
created_date: '2026-04-12 02:50'
updated_date: '2026-04-12 02:52'
labels:
- bug
- code-review
- release
- testing
dependencies: []
references:
- .github/workflows/prerelease.yml
- src/prerelease-workflow.test.ts
- src/core/services/overlay-shortcut.ts
parent_task_id: TASK-286
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Track the next unresolved CodeRabbit review threads on PR #49 after commit 62ad77dc and resolve the still-valid follow-up issues while documenting stale repeats.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All still-actionable CodeRabbit comments in the latest PR #49 round are fixed or explicitly shown stale with evidence.
- [x] #2 Regression coverage is updated for any workflow or test changes made in this round.
- [x] #3 Relevant verification passes for the touched workflow and prerelease test changes.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Assessed latest unresolved CodeRabbit round on PR #49. `src/core/services/overlay-shortcut.ts` comment is stale: `registerOverlayShortcuts()` returns `hasConfiguredOverlayShortcuts(shortcuts)`, so runtime registration is not hard-coded false.
Added exact, line-ending-agnostic prerelease tag trigger assertions in `src/prerelease-workflow.test.ts` and a regression asserting `bun run test:env` sits in the prerelease quality gate before source coverage.
Updated `.github/workflows/prerelease.yml` quality-gate to run `bun run test:env` after `bun run test:fast`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Assessed the latest CodeRabbit round for PR #49. Left the `overlay-shortcut.ts` thread open as stale with code evidence, tightened prerelease workflow trigger coverage, and added the missing `test:env` step to the prerelease quality gate. Verification: `bun test src/prerelease-workflow.test.ts`; `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,48 +0,0 @@
---
id: TASK-286.3
title: 'Assess and address PR #49 latest CodeRabbit review round'
status: Done
assignee: []
created_date: '2026-04-12 03:08'
updated_date: '2026-04-12 03:09'
labels:
- bug
- code-review
- testing
dependencies: []
references:
- 'PR #49'
- .github/workflows/prerelease.yml
- src
parent_task_id: TASK-286
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Track the newest unresolved CodeRabbit review threads on PR #49 after commit 942c1649, fix the still-valid issues, verify them, and push the branch update.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All still-actionable CodeRabbit comments in the newest PR #49 round are fixed or explicitly identified stale with evidence.
- [x] #2 Regression coverage is added or updated for behavior touched in this round.
- [x] #3 Relevant verification passes before commit and push.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Fetched the newest unresolved CodeRabbit threads for PR #49 after commit `942c1649`; only one unresolved actionable thread remained, on prerelease checksum output using repo-relative paths instead of asset basenames.
Added regression coverage in `src/prerelease-workflow.test.ts` and `src/release-workflow.test.ts` asserting checksum generation truncates to asset basenames and no longer writes the raw `sha256sum "${files[@]}" > release/SHA256SUMS.txt` form.
Updated both `.github/workflows/prerelease.yml` and `.github/workflows/release.yml` checksum generation steps to iterate over the `files` array and write `SHA256 basename` lines into `release/SHA256SUMS.txt`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Resolved the latest CodeRabbit round for PR #49 by fixing checksum generation to emit basename-oriented `SHA256SUMS.txt` entries in both prerelease and release workflows, with matching regression coverage. Verification: `bun test src/prerelease-workflow.test.ts src/release-workflow.test.ts`; `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,56 +0,0 @@
---
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
<!-- SECTION:DESCRIPTION:BEGIN -->
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.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [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.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
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.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
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.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
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.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,5 @@
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.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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.
+11
View File
@@ -0,0 +1,11 @@
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.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,7 @@
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.
@@ -0,0 +1,4 @@
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.
+4
View File
@@ -0,0 +1,4 @@
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.
+11
View File
@@ -0,0 +1,11 @@
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.
+4
View File
@@ -0,0 +1,4 @@
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.
+4 -47
View File
@@ -1,49 +1,6 @@
# 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.
## Previous Versions
<details>
<summary>v0.11.x</summary>
<h2>v0.11.2 (2026-04-07)</h2>
## 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.
@@ -53,13 +10,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.
<h2>v0.11.1 (2026-04-04)</h2>
## 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.
<h2>v0.11.0 (2026-04-03)</h2>
## 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.
@@ -112,7 +69,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.
</details>
## Previous Versions
<details>
<summary>v0.10.x</summary>
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "subminer",
"productName": "SubMiner",
"desktopName": "SubMiner.desktop",
"version": "0.12.0",
"version": "0.12.0-beta.3",
"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-release",
"changelog:build": "bun run scripts/build-changelog.ts build && bun run changelog:docs",
"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",
+2 -45
View File
@@ -139,49 +139,6 @@ 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');
@@ -405,11 +362,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,
/## Highlights\n### Added\n- Overlay: Added prerelease coverage\./,
/### Fixed\n- Launcher: Fixed prerelease packaging checks\./,
);
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 });
-20
View File
@@ -430,21 +430,6 @@ 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);
}
@@ -741,11 +726,6 @@ function main(): void {
return;
}
if (command === 'build-release') {
writeStableReleaseArtifacts(options);
return;
}
if (command === 'check') {
verifyChangelogReadyForRelease(options);
return;
+9 -2
View File
@@ -107,7 +107,11 @@ 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);
@@ -217,7 +221,10 @@ 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);
+1 -4
View File
@@ -173,10 +173,7 @@ 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 };
+3 -1
View File
@@ -75,7 +75,9 @@ 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 () => {
+6 -1
View File
@@ -277,7 +277,12 @@ 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) {
@@ -3840,7 +3840,16 @@ 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 [
@@ -3938,7 +3947,16 @@ 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(
`
@@ -414,7 +414,8 @@ 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,
});
@@ -606,15 +606,13 @@ 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;
@@ -171,12 +171,10 @@ 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;
}
+22 -40
View File
@@ -886,47 +886,29 @@ 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: 'copySubtitleMultiple',
payload: { count: 3 },
});
await dispatchHandler!({}, {
actionId: 'cycleRuntimeOption',
payload: {
runtimeOptionId: 'anki.autoUpdateNewCards',
direction: -1,
},
);
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',
},
);
});
await dispatchHandler!({}, {
actionId: 'toggleSubtitleSidebar',
});
await dispatchHandler!({}, {
actionId: 'openSessionHelp',
});
await dispatchHandler!({}, {
actionId: 'openControllerSelect',
});
await dispatchHandler!({}, {
actionId: 'openControllerDebug',
});
assert.deepEqual(dispatched, [
{
+5 -1
View File
@@ -45,7 +45,11 @@ 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);
+6 -12
View File
@@ -158,24 +158,18 @@ 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);
}
}
}
+17 -21
View File
@@ -54,7 +54,9 @@ 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(' '),
})),
[
{
@@ -189,10 +191,9 @@ 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', () => {
@@ -221,16 +222,12 @@ 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', () => {
@@ -244,10 +241,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', () => {
@@ -258,10 +255,9 @@ 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', () => {
+3 -1
View File
@@ -342,7 +342,9 @@ 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[];
} {
+19 -25
View File
@@ -415,10 +415,7 @@ 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';
@@ -1936,7 +1933,9 @@ 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 {
@@ -1946,9 +1945,11 @@ 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;
@@ -2254,16 +2255,14 @@ function openOverlayHostedModalWithOsd(
unavailableMessage: string,
failureLogMessage: string,
): void {
void openModal(createOverlayHostedModalOpenDeps())
.then((opened) => {
if (!opened) {
showMpvOsd(unavailableMessage);
}
})
.catch((error) => {
logger.error(failureLogMessage, error);
void openModal(createOverlayHostedModalOpenDeps()).then((opened) => {
if (!opened) {
showMpvOsd(unavailableMessage);
});
}
}).catch((error) => {
logger.error(failureLogMessage, error);
showMpvOsd(unavailableMessage);
});
}
function openRuntimeOptionsPalette(): void {
@@ -4933,8 +4932,7 @@ 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),
@@ -5202,18 +5200,14 @@ 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;
+1 -4
View File
@@ -132,10 +132,7 @@ 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,
+19 -24
View File
@@ -180,15 +180,13 @@ 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;
}
@@ -563,26 +561,23 @@ 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');
},
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => null,
getModalWindow: () => window as never,
createModalWindow: () => {
throw new Error('modal window should not be created when already present');
},
{
scheduleRevealFallback: (callback) => {
scheduledReveal = callback;
return { scheduled: true } as never;
},
clearRevealFallback: () => {
scheduledReveal = null;
},
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
}, {
scheduleRevealFallback: (callback) => {
scheduledReveal = callback;
return { scheduled: true } as never;
},
);
clearRevealFallback: () => {
scheduledReveal = null;
},
});
window.loading = true;
window.url = '';
+8 -2
View File
@@ -54,7 +54,10 @@ type RevealFallbackHandle = NonNullable<Parameters<typeof globalThis.clearTimeou
export interface OverlayModalRuntimeOptions {
onModalStateChange?: (isActive: boolean) => void;
scheduleRevealFallback?: (callback: () => void, delayMs: number) => RevealFallbackHandle;
scheduleRevealFallback?: (
callback: () => void,
delayMs: number,
) => RevealFallbackHandle;
clearRevealFallback?: (timeout: RevealFallbackHandle) => void;
}
@@ -70,7 +73,10 @@ 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);
@@ -16,8 +16,7 @@ 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'),
@@ -45,7 +45,11 @@ 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 {
@@ -94,7 +94,10 @@ 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,
+1 -3
View File
@@ -194,9 +194,7 @@ 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,
);
});
+1 -4
View File
@@ -105,10 +105,7 @@ 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();
+1 -3
View File
@@ -124,9 +124,7 @@ function createQueuedIpcListenerWithPayload<T>(
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<YoutubePickerOpenPayload>(
+3 -50
View File
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const prereleaseWorkflowPath = resolve(__dirname, '../.github/workflows/prerelease.yml');
const prereleaseWorkflow = readFileSync(prereleaseWorkflowPath, 'utf8').replace(/\r\n/g, '\n');
const prereleaseWorkflow = readFileSync(prereleaseWorkflowPath, 'utf8');
const packageJsonPath = resolve(__dirname, '../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
scripts: Record<string, string>;
@@ -12,12 +12,8 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
test('prerelease workflow triggers on beta and rc tags only', () => {
assert.match(prereleaseWorkflow, /name: Prerelease/);
const tagsBlock = prereleaseWorkflow.match(/tags:\s*\n((?:\s*-\s*'[^']+'\s*\n?)+)/);
assert.ok(tagsBlock, 'Workflow tags block not found');
const tagsText = tagsBlock[1];
assert.ok(tagsText, 'Workflow tags entries not found');
const tagPatterns = [...tagsText.matchAll(/-\s*'([^']+)'/g)].map(([, pattern]) => pattern);
assert.deepEqual(tagPatterns, ['v*-beta.*', 'v*-rc.*']);
assert.match(prereleaseWorkflow, /tags:\s*\n\s*-\s*'v\*-beta\.\*'/);
assert.match(prereleaseWorkflow, /tags:\s*\n(?:.*\n)*\s*-\s*'v\*-rc\.\*'/);
});
test('package scripts expose prerelease notes generation separately from stable changelog build', () => {
@@ -32,30 +28,12 @@ test('prerelease workflow generates prerelease notes from pending fragments', ()
assert.doesNotMatch(prereleaseWorkflow, /bun run changelog:build --version/);
});
test('prerelease workflow includes the environment suite in the gate sequence', () => {
assert.match(
prereleaseWorkflow,
/Test suite \(source\)\n\s*run: bun run test:fast\n\s*\n\s*- name: Environment suite(?: \(source\))?\n\s*run: bun run test:env\n\s*\n\s*- name: Coverage suite \(maintained source lane\)/,
);
});
test('prerelease workflow publishes GitHub prereleases and keeps them off latest', () => {
assert.match(prereleaseWorkflow, /gh release edit[\s\S]*--prerelease/);
assert.match(prereleaseWorkflow, /gh release create[\s\S]*--prerelease/);
assert.match(prereleaseWorkflow, /gh release create[\s\S]*--latest=false/);
});
test('prerelease workflow scopes dependency caches by runner architecture', () => {
const archScopedCacheKeyMatches = prereleaseWorkflow.match(
/key:\s*\${{\s*runner\.os\s*}}-\${{\s*runner\.arch\s*}}-bun-/g,
);
const archScopedRestoreKeyMatches = prereleaseWorkflow.match(
/\${{\s*runner\.os\s*}}-\${{\s*runner\.arch\s*}}-bun-/g,
);
assert.equal(archScopedCacheKeyMatches?.length, 5);
assert.ok((archScopedRestoreKeyMatches?.length ?? 0) >= 10);
});
test('prerelease workflow builds and uploads all release platforms', () => {
assert.match(prereleaseWorkflow, /build-linux:/);
assert.match(prereleaseWorkflow, /build-macos:/);
@@ -76,31 +54,6 @@ test('prerelease workflow publishes the same release assets as the stable workfl
);
});
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/,
);
});
test('prerelease workflow validates artifacts before publishing the release and only undrafts after upload', () => {
const artifactsIndex = prereleaseWorkflow.indexOf('artifacts=(');
const createIndex = prereleaseWorkflow.indexOf('gh release create');
const uploadIndex = prereleaseWorkflow.indexOf('gh release upload');
const undraftIndex = prereleaseWorkflow.indexOf('--draft=false');
assert.notEqual(artifactsIndex, -1);
assert.notEqual(createIndex, -1);
assert.notEqual(uploadIndex, -1);
assert.notEqual(undraftIndex, -1);
assert.ok(artifactsIndex < createIndex);
assert.ok(uploadIndex < undraftIndex);
assert.match(prereleaseWorkflow, /gh release create[\s\S]*--draft[\s\S]*--prerelease/);
});
test('prerelease workflow does not publish to AUR', () => {
assert.doesNotMatch(prereleaseWorkflow, /aur-publish:/);
assert.doesNotMatch(prereleaseWorkflow, /AUR_SSH_PRIVATE_KEY/);
-7
View File
@@ -77,13 +77,6 @@ test('release workflow includes the Windows installer in checksums and uploaded
);
});
test('release workflow writes checksum entries using release asset basenames', () => {
assert.match(releaseWorkflow, /: > release\/SHA256SUMS\.txt/);
assert.match(releaseWorkflow, /for file in "\$\{files\[@\]\}"; do/);
assert.match(releaseWorkflow, /\$\{file##\*\/\}/);
assert.doesNotMatch(releaseWorkflow, /sha256sum "\$\{files\[@\]\}" > release\/SHA256SUMS\.txt/);
});
test('release package scripts disable implicit electron-builder publishing', () => {
assert.match(packageJson.scripts['build:appimage'] ?? '', /--publish never/);
assert.match(packageJson.scripts['build:mac'] ?? '', /--publish never/);
+1 -4
View File
@@ -364,10 +364,7 @@ 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];
+1 -3
View File
@@ -1063,9 +1063,7 @@ 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();
}
+6 -4
View File
@@ -39,10 +39,12 @@ export function createKeyboardHandlers(
let pendingLookupRefreshAfterSubtitleSeek = false;
let resetSelectionToStartOnNextSubtitleSync = false;
let lookupScanFallbackTimer: ReturnType<typeof setTimeout> | null = null;
let pendingNumericSelection: {
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
timeout: ReturnType<typeof setTimeout> | null;
} | null = null;
let pendingNumericSelection:
| {
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
timeout: ReturnType<typeof setTimeout> | null;
}
| null = null;
const CHORD_MAP = new Map<
string,
+5 -7
View File
@@ -73,13 +73,11 @@ 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);
+35 -41
View File
@@ -168,54 +168,48 @@ function withRuntimeOptionsModal(
test('openRuntimeOptionsModal shows loading shell before runtime options resolve', async () => {
const deferred = createDeferred<RuntimeOptionState[]>();
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<RuntimeOptionState[]>();
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);
});
});
+1 -2
View File
@@ -130,8 +130,7 @@ 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' }]
: [],
},
+1 -4
View File
@@ -73,10 +73,7 @@ function queryPopupElements<T extends Element>(
}
export function isYomitanPopupVisible(root: ParentNode | null | undefined = document): boolean {
const visiblePopupHosts = queryPopupElements<HTMLElement>(
root,
YOMITAN_POPUP_VISIBLE_HOST_SELECTOR,
);
const visiblePopupHosts = queryPopupElements<HTMLElement>(root, YOMITAN_POPUP_VISIBLE_HOST_SELECTOR);
if (visiblePopupHosts.length > 0) {
return true;
}
+1 -3
View File
@@ -256,9 +256,7 @@ 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<string | number> | null {
+1 -4
View File
@@ -364,10 +364,7 @@ export interface ElectronAPI {
getKeybindings: () => Promise<Keybinding[]>;
getSessionBindings: () => Promise<CompiledSessionBinding[]>;
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
dispatchSessionAction: (
actionId: SessionActionId,
payload?: SessionActionPayload,
) => Promise<void>;
dispatchSessionAction: (actionId: SessionActionId, payload?: SessionActionPayload) => Promise<void>;
getStatsToggleKey: () => Promise<string>;
getMarkWatchedKey: () => Promise<string>;
markActiveVideoWatched: () => Promise<boolean>;
+3 -1
View File
@@ -62,7 +62,9 @@ export interface CompiledSessionActionBinding extends CompiledSessionBindingBase
payload?: SessionActionPayload;
}
export type CompiledSessionBinding = CompiledMpvCommandBinding | CompiledSessionActionBinding;
export type CompiledSessionBinding =
| CompiledMpvCommandBinding
| CompiledSessionActionBinding;
export interface PluginSessionBindingsArtifact {
version: 1;
+2 -18
View File
@@ -1,9 +1,6 @@
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<string | null>): MpvPollResult {
@@ -54,19 +51,6 @@ 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');
});
test('matchesMpvSocketPathInCommandLine rejects socket-path prefix matches', () => {
assert.equal(
matchesMpvSocketPathInCommandLine(
'mpv.exe --input-ipc-server=\\\\.\\pipe\\subminer-10 video.mkv',
'\\\\.\\pipe\\subminer-1',
),
false,
);
});
+3 -4
View File
@@ -13,10 +13,9 @@ export function matchesMpvSocketPathInCommandLine(
}
const escapedSocketPath = escapeRegex(targetSocketPath);
return new RegExp(
`(?:^|\\s)--input-ipc-server(?:=|\\s+)(?:"${escapedSocketPath}"|${escapedSocketPath})(?=\\s|$)`,
'i',
).test(commandLine);
return new RegExp(`--input-ipc-server(?:=|\\s+)("?${escapedSocketPath}"?)`, 'i').test(
commandLine,
);
}
export function filterMpvPollResultBySocketPath(
+2 -6
View File
@@ -173,7 +173,7 @@ function getProcessNameByPid(pid: number): string | null {
}
}
const processCommandLineCache = new Map<number, string>();
const processCommandLineCache = new Map<number, string | null>();
function getProcessCommandLineByPid(pid: number): string | null {
if (processCommandLineCache.has(pid)) {
@@ -204,11 +204,7 @@ function getProcessCommandLineByPid(pid: number): string | null {
commandLine = null;
}
if (commandLine !== null) {
processCommandLineCache.set(pid, commandLine);
} else {
processCommandLineCache.delete(pid);
}
processCommandLineCache.set(pid, commandLine);
return commandLine;
}
+1 -2
View File
@@ -26,8 +26,7 @@ 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;
}
+1 -3
View File
@@ -4,9 +4,7 @@ import { WindowsWindowTracker } from './windows-tracker';
import type { MpvPollResult } from './win32';
function mpvVisible(
overrides: Partial<
MpvPollResult & { x?: number; y?: number; width?: number; height?: number; focused?: boolean }
> = {},
overrides: Partial<MpvPollResult & { x?: number; y?: number; width?: number; height?: number; focused?: boolean }> = {},
): MpvPollResult {
return {
matches: [
+1 -2
View File
@@ -55,8 +55,7 @@ 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(