mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-12 04:19:25 -07:00
chore(release): prepare v0.12.0
This commit is contained in:
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,5 +1,43 @@
|
|||||||
# Changelog
|
# 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)
|
## v0.11.2 (2026-04-07)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
<!-- 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 -->
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -1,6 +1,49 @@
|
|||||||
# Changelog
|
# 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
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>v0.11.x</summary>
|
||||||
|
|
||||||
|
<h2>v0.11.2 (2026-04-07)</h2>
|
||||||
|
|
||||||
**Changed**
|
**Changed**
|
||||||
- Launcher: Replaced the launcher-only fullscreen toggle with `mpv.launchMode` so SubMiner-managed mpv playback can start in normal, maximized, or fullscreen mode.
|
- 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.
|
- 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.
|
- 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)
|
<h2>v0.11.1 (2026-04-04)</h2>
|
||||||
|
|
||||||
**Fixed**
|
**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`.
|
- 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.
|
- 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)
|
<h2>v0.11.0 (2026-04-03)</h2>
|
||||||
|
|
||||||
**Added**
|
**Added**
|
||||||
- Overlay: Added a playlist browser overlay modal for browsing sibling video files and the live mpv queue during playback.
|
- 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: 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.
|
- Release: Updated Electron to 39.8.6 and pinned patched transitive build dependencies to clear the reported high-severity audit findings.
|
||||||
|
|
||||||
## Previous Versions
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>v0.10.x</summary>
|
<summary>v0.10.x</summary>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "subminer",
|
"name": "subminer",
|
||||||
"productName": "SubMiner",
|
"productName": "SubMiner",
|
||||||
"desktopName": "SubMiner.desktop",
|
"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",
|
"description": "All-in-one sentence mining overlay with AnkiConnect and dictionary integration",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"main": "dist/main-entry.js",
|
"main": "dist/main-entry.js",
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"dev:stats": "cd stats && bun run dev",
|
"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": "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",
|
"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:check": "bun run scripts/build-changelog.ts check",
|
||||||
"changelog:docs": "bun run scripts/build-changelog.ts docs",
|
"changelog:docs": "bun run scripts/build-changelog.ts docs",
|
||||||
"changelog:lint": "bun run scripts/build-changelog.ts lint",
|
"changelog:lint": "bun run scripts/build-changelog.ts lint",
|
||||||
|
|||||||
@@ -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 () => {
|
test('verifyChangelogReadyForRelease ignores README but rejects pending fragments and missing version sections', async () => {
|
||||||
const { verifyChangelogReadyForRelease } = await loadModule();
|
const { verifyChangelogReadyForRelease } = await loadModule();
|
||||||
const workspace = createWorkspace('verify-release');
|
const workspace = createWorkspace('verify-release');
|
||||||
@@ -362,11 +405,11 @@ test('writePrereleaseNotesForVersion writes cumulative beta notes without mutati
|
|||||||
|
|
||||||
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
|
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
|
||||||
assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m);
|
assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m);
|
||||||
assert.match(prereleaseNotes, /## Highlights\n### Added\n- Overlay: Added prerelease coverage\./);
|
|
||||||
assert.match(
|
assert.match(
|
||||||
prereleaseNotes,
|
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/);
|
assert.match(prereleaseNotes, /## Installation\n\nSee the README and docs\/installation guide/);
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(workspace, { recursive: true, force: true });
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -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 {
|
export function verifyChangelogFragments(options?: ChangelogOptions): void {
|
||||||
readChangeFragments(options?.cwd ?? process.cwd(), options?.deps);
|
readChangeFragments(options?.cwd ?? process.cwd(), options?.deps);
|
||||||
}
|
}
|
||||||
@@ -726,6 +741,11 @@ function main(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command === 'build-release') {
|
||||||
|
writeStableReleaseArtifacts(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (command === 'check') {
|
if (command === 'check') {
|
||||||
verifyChangelogReadyForRelease(options);
|
verifyChangelogReadyForRelease(options);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -107,11 +107,7 @@ test('parseArgs captures session action forwarding flags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseArgs ignores non-positive numeric session action counts', () => {
|
test('parseArgs ignores non-positive numeric session action counts', () => {
|
||||||
const args = parseArgs([
|
const args = parseArgs(['--copy-subtitle-count=0', '--mine-sentence-count', '-1']);
|
||||||
'--copy-subtitle-count=0',
|
|
||||||
'--mine-sentence-count',
|
|
||||||
'-1',
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.equal(args.copySubtitleCount, undefined);
|
assert.equal(args.copySubtitleCount, undefined);
|
||||||
assert.equal(args.mineSentenceCount, undefined);
|
assert.equal(args.mineSentenceCount, undefined);
|
||||||
@@ -221,10 +217,7 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => {
|
|||||||
assert.equal(hasExplicitCommand(toggleStatsOverlay), true);
|
assert.equal(hasExplicitCommand(toggleStatsOverlay), true);
|
||||||
assert.equal(shouldStartApp(toggleStatsOverlay), true);
|
assert.equal(shouldStartApp(toggleStatsOverlay), true);
|
||||||
|
|
||||||
const cycleRuntimeOption = parseArgs([
|
const cycleRuntimeOption = parseArgs(['--cycle-runtime-option', 'anki.autoUpdateNewCards:next']);
|
||||||
'--cycle-runtime-option',
|
|
||||||
'anki.autoUpdateNewCards:next',
|
|
||||||
]);
|
|
||||||
assert.equal(cycleRuntimeOption.cycleRuntimeOptionId, 'anki.autoUpdateNewCards');
|
assert.equal(cycleRuntimeOption.cycleRuntimeOptionId, 'anki.autoUpdateNewCards');
|
||||||
assert.equal(cycleRuntimeOption.cycleRuntimeOptionDirection, 1);
|
assert.equal(cycleRuntimeOption.cycleRuntimeOptionDirection, 1);
|
||||||
assert.equal(hasExplicitCommand(cycleRuntimeOption), true);
|
assert.equal(hasExplicitCommand(cycleRuntimeOption), true);
|
||||||
|
|||||||
@@ -173,7 +173,10 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
const separatorIndex = value.lastIndexOf(':');
|
const separatorIndex = value.lastIndexOf(':');
|
||||||
if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null;
|
if (separatorIndex <= 0 || separatorIndex === value.length - 1) return null;
|
||||||
const id = value.slice(0, separatorIndex).trim();
|
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 (!id) return null;
|
||||||
if (rawDirection === 'next' || rawDirection === '1') {
|
if (rawDirection === 'next' || rawDirection === '1') {
|
||||||
return { id, direction: 1 };
|
return { id, direction: 1 };
|
||||||
|
|||||||
@@ -75,9 +75,7 @@ test('runAppReadyRuntime starts websocket in auto mode when plugin missing', asy
|
|||||||
calls.indexOf('setVisibleOverlayVisible:true') < calls.indexOf('initializeOverlayRuntime'),
|
calls.indexOf('setVisibleOverlayVisible:true') < calls.indexOf('initializeOverlayRuntime'),
|
||||||
);
|
);
|
||||||
assert.ok(calls.includes('startBackgroundWarmups'));
|
assert.ok(calls.includes('startBackgroundWarmups'));
|
||||||
assert.ok(
|
assert.ok(calls.includes('log:Runtime ready: immersion tracker startup requested.'));
|
||||||
calls.includes('log:Runtime ready: immersion tracker startup requested.'),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runAppReadyRuntime starts texthooker on startup when enabled in config', async () => {
|
test('runAppReadyRuntime starts texthooker on startup when enabled in config', async () => {
|
||||||
|
|||||||
@@ -277,12 +277,7 @@ export function handleCliCommand(
|
|||||||
logLabel: string,
|
logLabel: string,
|
||||||
osdLabel: string,
|
osdLabel: string,
|
||||||
): void => {
|
): void => {
|
||||||
runAsyncWithOsd(
|
runAsyncWithOsd(() => deps.dispatchSessionAction(request), deps, logLabel, osdLabel);
|
||||||
() => deps.dispatchSessionAction(request),
|
|
||||||
deps,
|
|
||||||
logLabel,
|
|
||||||
osdLabel,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (args.logLevel) {
|
if (args.logLevel) {
|
||||||
|
|||||||
@@ -3840,16 +3840,7 @@ test('getTrendsDashboard builds librarySummary with per-title aggregates', () =>
|
|||||||
lines_seen = ?, tokens_seen = ?, cards_mined = ?, yomitan_lookup_count = ?
|
lines_seen = ?, tokens_seen = ?, cards_mined = ?, yomitan_lookup_count = ?
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
`,
|
`,
|
||||||
).run(
|
).run(`${startedAtMs + activeMs}`, activeMs, activeMs, 10, tokens, cards, lookups, sessionId);
|
||||||
`${startedAtMs + activeMs}`,
|
|
||||||
activeMs,
|
|
||||||
activeMs,
|
|
||||||
10,
|
|
||||||
tokens,
|
|
||||||
cards,
|
|
||||||
lookups,
|
|
||||||
sessionId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [day, active, tokens, cards] of [
|
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 = ?
|
lines_seen = ?, tokens_seen = ?, cards_mined = ?, yomitan_lookup_count = ?
|
||||||
WHERE session_id = ?
|
WHERE session_id = ?
|
||||||
`,
|
`,
|
||||||
).run(
|
).run(`${startMs + 20 * 60_000}`, 20 * 60_000, 20 * 60_000, 5, 0, 0, 0, session.sessionId);
|
||||||
`${startMs + 20 * 60_000}`,
|
|
||||||
20 * 60_000,
|
|
||||||
20 * 60_000,
|
|
||||||
5,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
session.sessionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -414,8 +414,7 @@ function buildLibrarySummary(
|
|||||||
cards: acc.cards,
|
cards: acc.cards,
|
||||||
words: acc.words,
|
words: acc.words,
|
||||||
lookups: acc.lookups,
|
lookups: acc.lookups,
|
||||||
lookupsPerHundred:
|
lookupsPerHundred: acc.words > 0 ? +((acc.lookups / acc.words) * 100).toFixed(1) : null,
|
||||||
acc.words > 0 ? +((acc.lookups / acc.words) * 100).toFixed(1) : null,
|
|
||||||
firstWatched: acc.firstWatched,
|
firstWatched: acc.firstWatched,
|
||||||
lastWatched: acc.lastWatched,
|
lastWatched: acc.lastWatched,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
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
|
SELECT ts_ms AS tsMs, typeof(ts_ms) AS tsType, CREATED_DATE AS createdDate
|
||||||
FROM imm_session_events
|
FROM imm_session_events
|
||||||
WHERE event_id = 1
|
WHERE event_id = 1
|
||||||
`,
|
`,
|
||||||
).get() as {
|
)
|
||||||
|
.get() as {
|
||||||
tsMs: string;
|
tsMs: string;
|
||||||
tsType: string;
|
tsType: string;
|
||||||
createdDate: string;
|
createdDate: string;
|
||||||
|
|||||||
@@ -171,10 +171,12 @@ function hasColumn(db: DatabaseSync, tableName: string, columnName: string): boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getColumnType(db: DatabaseSync, tableName: string, columnName: string): string | null {
|
function getColumnType(db: DatabaseSync, tableName: string, columnName: string): string | null {
|
||||||
const row = (db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
|
const row = (
|
||||||
name: string;
|
db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
|
||||||
type: string;
|
name: string;
|
||||||
}>).find((entry) => entry.name === columnName);
|
type: string;
|
||||||
|
}>
|
||||||
|
).find((entry) => entry.name === columnName);
|
||||||
return row?.type ?? null;
|
return row?.type ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -886,29 +886,47 @@ test('registerIpcHandlers validates dispatchSessionAction payloads', async () =>
|
|||||||
await dispatchHandler!({}, { actionId: 'unknown-action' });
|
await dispatchHandler!({}, { actionId: 'unknown-action' });
|
||||||
}, /Invalid session action payload/);
|
}, /Invalid session action payload/);
|
||||||
|
|
||||||
await dispatchHandler!({}, {
|
await dispatchHandler!(
|
||||||
actionId: 'copySubtitleMultiple',
|
{},
|
||||||
payload: { count: 3 },
|
{
|
||||||
});
|
actionId: 'copySubtitleMultiple',
|
||||||
await dispatchHandler!({}, {
|
payload: { count: 3 },
|
||||||
actionId: 'cycleRuntimeOption',
|
|
||||||
payload: {
|
|
||||||
runtimeOptionId: 'anki.autoUpdateNewCards',
|
|
||||||
direction: -1,
|
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
await dispatchHandler!({}, {
|
await dispatchHandler!(
|
||||||
actionId: 'toggleSubtitleSidebar',
|
{},
|
||||||
});
|
{
|
||||||
await dispatchHandler!({}, {
|
actionId: 'cycleRuntimeOption',
|
||||||
actionId: 'openSessionHelp',
|
payload: {
|
||||||
});
|
runtimeOptionId: 'anki.autoUpdateNewCards',
|
||||||
await dispatchHandler!({}, {
|
direction: -1,
|
||||||
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, [
|
assert.deepEqual(dispatched, [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ test('collectDroppedVideoPaths parses text/uri-list entries and de-duplicates',
|
|||||||
|
|
||||||
test('collectDroppedSubtitlePaths keeps supported dropped subtitle paths in order', () => {
|
test('collectDroppedSubtitlePaths keeps supported dropped subtitle paths in order', () => {
|
||||||
const transfer = makeTransfer({
|
const transfer = makeTransfer({
|
||||||
files: [
|
files: [{ path: '/subs/ep02.ass' }, { path: '/subs/readme.txt' }, { path: '/subs/ep03.SRT' }],
|
||||||
{ path: '/subs/ep02.ass' },
|
|
||||||
{ path: '/subs/readme.txt' },
|
|
||||||
{ path: '/subs/ep03.SRT' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = collectDroppedSubtitlePaths(transfer);
|
const result = collectDroppedSubtitlePaths(transfer);
|
||||||
|
|||||||
@@ -158,18 +158,24 @@ export function updateVisibleOverlayVisibility(args: {
|
|||||||
setOverlayWindowOpacity(mainWindow, 0);
|
setOverlayWindowOpacity(mainWindow, 0);
|
||||||
mainWindow.showInactive();
|
mainWindow.showInactive();
|
||||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||||
scheduleWindowsOverlayReveal(mainWindow, shouldBindTrackedWindowsOverlay
|
scheduleWindowsOverlayReveal(
|
||||||
? (window) => args.syncWindowsOverlayToMpvZOrder?.(window)
|
mainWindow,
|
||||||
: undefined);
|
shouldBindTrackedWindowsOverlay
|
||||||
|
? (window) => args.syncWindowsOverlayToMpvZOrder?.(window)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (args.isWindowsPlatform) {
|
if (args.isWindowsPlatform) {
|
||||||
setOverlayWindowOpacity(mainWindow, 0);
|
setOverlayWindowOpacity(mainWindow, 0);
|
||||||
}
|
}
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
if (args.isWindowsPlatform) {
|
if (args.isWindowsPlatform) {
|
||||||
scheduleWindowsOverlayReveal(mainWindow, shouldBindTrackedWindowsOverlay
|
scheduleWindowsOverlayReveal(
|
||||||
? (window) => args.syncWindowsOverlayToMpvZOrder?.(window)
|
mainWindow,
|
||||||
: undefined);
|
shouldBindTrackedWindowsOverlay
|
||||||
|
? (window) => args.syncWindowsOverlayToMpvZOrder?.(window)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ test('compileSessionBindings merges shortcuts and keybindings into one canonical
|
|||||||
code: binding.key.code,
|
code: binding.key.code,
|
||||||
modifiers: binding.key.modifiers,
|
modifiers: binding.key.modifiers,
|
||||||
target:
|
target:
|
||||||
binding.actionType === 'session-action'
|
binding.actionType === 'session-action' ? binding.actionId : binding.command.join(' '),
|
||||||
? binding.actionId
|
|
||||||
: binding.command.join(' '),
|
|
||||||
})),
|
})),
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -191,9 +189,10 @@ test('compileSessionBindings omits disabled bindings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.warnings.length, 0);
|
assert.equal(result.warnings.length, 0);
|
||||||
assert.deepEqual(result.bindings.map((binding) => binding.sourcePath), [
|
assert.deepEqual(
|
||||||
'shortcuts.toggleVisibleOverlayGlobal',
|
result.bindings.map((binding) => binding.sourcePath),
|
||||||
]);
|
['shortcuts.toggleVisibleOverlayGlobal'],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('compileSessionBindings warns on unsupported shortcut and keybinding syntax', () => {
|
test('compileSessionBindings warns on unsupported shortcut and keybinding syntax', () => {
|
||||||
@@ -222,12 +221,16 @@ test('compileSessionBindings rejects malformed command arrays', () => {
|
|||||||
platform: 'linux',
|
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.equal(result.bindings[0]?.actionType, 'mpv-command');
|
||||||
assert.deepEqual(result.bindings[0]?.command, ['show-text', 3000]);
|
assert.deepEqual(result.bindings[0]?.command, ['show-text', 3000]);
|
||||||
assert.deepEqual(result.warnings.map((warning) => `${warning.kind}:${warning.path}`), [
|
assert.deepEqual(
|
||||||
'unsupported:keybindings[1].command',
|
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', () => {
|
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.bindings, []);
|
||||||
assert.deepEqual(result.warnings.map((warning) => `${warning.kind}:${warning.path}`), [
|
assert.deepEqual(
|
||||||
'unsupported:keybindings[0].command',
|
result.warnings.map((warning) => `${warning.kind}:${warning.path}`),
|
||||||
'unsupported:keybindings[1].command',
|
['unsupported:keybindings[0].command', 'unsupported:keybindings[1].command'],
|
||||||
]);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('compileSessionBindings points unsupported command warnings at the command field', () => {
|
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.bindings, []);
|
||||||
assert.deepEqual(result.warnings.map((warning) => `${warning.kind}:${warning.path}`), [
|
assert.deepEqual(
|
||||||
'unsupported:keybindings[0].command',
|
result.warnings.map((warning) => `${warning.kind}:${warning.path}`),
|
||||||
]);
|
['unsupported:keybindings[0].command'],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('compileSessionBindings warns on deprecated toggleVisibleOverlayGlobal config', () => {
|
test('compileSessionBindings warns on deprecated toggleVisibleOverlayGlobal config', () => {
|
||||||
|
|||||||
@@ -342,9 +342,7 @@ function getBindingFingerprint(binding: CompiledSessionBinding): string {
|
|||||||
return `session:${binding.actionId}:${JSON.stringify(binding.payload ?? null)}`;
|
return `session:${binding.actionId}:${JSON.stringify(binding.payload ?? null)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compileSessionBindings(
|
export function compileSessionBindings(input: CompileSessionBindingsInput): {
|
||||||
input: CompileSessionBindingsInput,
|
|
||||||
): {
|
|
||||||
bindings: CompiledSessionBinding[];
|
bindings: CompiledSessionBinding[];
|
||||||
warnings: SessionBindingWarning[];
|
warnings: SessionBindingWarning[];
|
||||||
} {
|
} {
|
||||||
|
|||||||
44
src/main.ts
44
src/main.ts
@@ -415,7 +415,10 @@ import { createAnilistRateLimiter } from './core/services/anilist/rate-limiter';
|
|||||||
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
|
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
|
||||||
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
||||||
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
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 { dispatchSessionAction as dispatchSessionActionCore } from './core/services/session-actions';
|
||||||
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts';
|
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts';
|
||||||
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
||||||
@@ -1933,9 +1936,7 @@ function getWindowsNativeWindowHandle(window: BrowserWindow): string {
|
|||||||
|
|
||||||
function getWindowsNativeWindowHandleNumber(window: BrowserWindow): number {
|
function getWindowsNativeWindowHandleNumber(window: BrowserWindow): number {
|
||||||
const handle = window.getNativeWindowHandle();
|
const handle = window.getNativeWindowHandle();
|
||||||
return handle.length >= 8
|
return handle.length >= 8 ? Number(handle.readBigUInt64LE(0)) : handle.readUInt32LE(0);
|
||||||
? Number(handle.readBigUInt64LE(0))
|
|
||||||
: handle.readUInt32LE(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | null): number | null {
|
function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | null): number | null {
|
||||||
@@ -1945,11 +1946,9 @@ function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | nu
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetMpvSocketPath) {
|
if (targetMpvSocketPath) {
|
||||||
const windowTracker = appState.windowTracker as
|
const windowTracker = appState.windowTracker as {
|
||||||
| {
|
getTargetWindowHandle?: () => number | null;
|
||||||
getTargetWindowHandle?: () => number | null;
|
} | null;
|
||||||
}
|
|
||||||
| null;
|
|
||||||
const trackedHandle = windowTracker?.getTargetWindowHandle?.();
|
const trackedHandle = windowTracker?.getTargetWindowHandle?.();
|
||||||
if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) {
|
if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) {
|
||||||
return trackedHandle;
|
return trackedHandle;
|
||||||
@@ -2255,14 +2254,16 @@ function openOverlayHostedModalWithOsd(
|
|||||||
unavailableMessage: string,
|
unavailableMessage: string,
|
||||||
failureLogMessage: string,
|
failureLogMessage: string,
|
||||||
): void {
|
): void {
|
||||||
void openModal(createOverlayHostedModalOpenDeps()).then((opened) => {
|
void openModal(createOverlayHostedModalOpenDeps())
|
||||||
if (!opened) {
|
.then((opened) => {
|
||||||
|
if (!opened) {
|
||||||
|
showMpvOsd(unavailableMessage);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(failureLogMessage, error);
|
||||||
showMpvOsd(unavailableMessage);
|
showMpvOsd(unavailableMessage);
|
||||||
}
|
});
|
||||||
}).catch((error) => {
|
|
||||||
logger.error(failureLogMessage, error);
|
|
||||||
showMpvOsd(unavailableMessage);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRuntimeOptionsPalette(): void {
|
function openRuntimeOptionsPalette(): void {
|
||||||
@@ -4932,7 +4933,8 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
|
|||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
stopApp: () => requestAppQuit(),
|
stopApp: () => requestAppQuit(),
|
||||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||||
dispatchSessionAction: (request: SessionActionDispatchRequest) => dispatchSessionAction(request),
|
dispatchSessionAction: (request: SessionActionDispatchRequest) =>
|
||||||
|
dispatchSessionAction(request),
|
||||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||||
logInfo: (message: string) => logger.info(message),
|
logInfo: (message: string) => logger.info(message),
|
||||||
@@ -5200,14 +5202,18 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
|||||||
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) return;
|
||||||
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow);
|
||||||
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(appState.mpvSocketPath);
|
const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(appState.mpvSocketPath);
|
||||||
if (targetWindowHwnd !== null && bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)) {
|
if (
|
||||||
|
targetWindowHwnd !== null &&
|
||||||
|
bindWindowsOverlayAboveMpv(overlayHwnd, targetWindowHwnd)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tracker = appState.windowTracker;
|
const tracker = appState.windowTracker;
|
||||||
const mpvResult = tracker
|
const mpvResult = tracker
|
||||||
? (() => {
|
? (() => {
|
||||||
try {
|
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 poll = win32.findMpvWindows();
|
||||||
const focused = poll.matches.find((m) => m.isForeground);
|
const focused = poll.matches.find((m) => m.isForeground);
|
||||||
return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null;
|
return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null;
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ export function createMainBootServices<
|
|||||||
TSubtitleWebSocket,
|
TSubtitleWebSocket,
|
||||||
TLogger,
|
TLogger,
|
||||||
TRuntimeRegistry,
|
TRuntimeRegistry,
|
||||||
TOverlayManager extends { getMainWindow: () => BrowserWindow | null; getModalWindow: () => BrowserWindow | null },
|
TOverlayManager extends {
|
||||||
|
getMainWindow: () => BrowserWindow | null;
|
||||||
|
getModalWindow: () => BrowserWindow | null;
|
||||||
|
},
|
||||||
TOverlayModalInputState extends OverlayModalInputStateShape,
|
TOverlayModalInputState extends OverlayModalInputStateShape,
|
||||||
TOverlayContentMeasurementStore,
|
TOverlayContentMeasurementStore,
|
||||||
TOverlayModalRuntime,
|
TOverlayModalRuntime,
|
||||||
|
|||||||
@@ -180,13 +180,15 @@ function createMockWindow(): MockWindow & {
|
|||||||
get: () => state.contentReady,
|
get: () => state.contentReady,
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
state.contentReady = value;
|
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;
|
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 () => {
|
test('modal fallback reveal skips showing window when content is not ready', async () => {
|
||||||
const window = createMockWindow();
|
const window = createMockWindow();
|
||||||
let scheduledReveal: (() => void) | null = null;
|
let scheduledReveal: (() => void) | null = null;
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService(
|
||||||
getMainWindow: () => null,
|
{
|
||||||
getModalWindow: () => window as never,
|
getMainWindow: () => null,
|
||||||
createModalWindow: () => {
|
getModalWindow: () => window as never,
|
||||||
throw new Error('modal window should not be created when already present');
|
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;
|
||||||
scheduleRevealFallback: (callback) => {
|
return { scheduled: true } as never;
|
||||||
scheduledReveal = callback;
|
},
|
||||||
return { scheduled: true } as never;
|
clearRevealFallback: () => {
|
||||||
|
scheduledReveal = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
clearRevealFallback: () => {
|
);
|
||||||
scheduledReveal = null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
window.loading = true;
|
window.loading = true;
|
||||||
window.url = '';
|
window.url = '';
|
||||||
|
|||||||
@@ -54,10 +54,7 @@ type RevealFallbackHandle = NonNullable<Parameters<typeof globalThis.clearTimeou
|
|||||||
|
|
||||||
export interface OverlayModalRuntimeOptions {
|
export interface OverlayModalRuntimeOptions {
|
||||||
onModalStateChange?: (isActive: boolean) => void;
|
onModalStateChange?: (isActive: boolean) => void;
|
||||||
scheduleRevealFallback?: (
|
scheduleRevealFallback?: (callback: () => void, delayMs: number) => RevealFallbackHandle;
|
||||||
callback: () => void,
|
|
||||||
delayMs: number,
|
|
||||||
) => RevealFallbackHandle;
|
|
||||||
clearRevealFallback?: (timeout: RevealFallbackHandle) => void;
|
clearRevealFallback?: (timeout: RevealFallbackHandle) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +70,7 @@ export function createOverlayModalRuntimeService(
|
|||||||
let modalWindowPrimedForImmediateShow = false;
|
let modalWindowPrimedForImmediateShow = false;
|
||||||
let pendingModalWindowReveal: BrowserWindow | null = null;
|
let pendingModalWindowReveal: BrowserWindow | null = null;
|
||||||
let pendingModalWindowRevealTimeout: RevealFallbackHandle | null = null;
|
let pendingModalWindowRevealTimeout: RevealFallbackHandle | null = null;
|
||||||
const scheduleRevealFallback = (
|
const scheduleRevealFallback = (callback: () => void, delayMs: number): RevealFallbackHandle =>
|
||||||
callback: () => void,
|
|
||||||
delayMs: number,
|
|
||||||
): RevealFallbackHandle =>
|
|
||||||
(options.scheduleRevealFallback ?? globalThis.setTimeout)(callback, delayMs);
|
(options.scheduleRevealFallback ?? globalThis.setTimeout)(callback, delayMs);
|
||||||
const clearRevealFallback = (timeout: RevealFallbackHandle): void =>
|
const clearRevealFallback = (timeout: RevealFallbackHandle): void =>
|
||||||
(options.clearRevealFallback ?? globalThis.clearTimeout)(timeout);
|
(options.clearRevealFallback ?? globalThis.clearTimeout)(timeout);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
|
|||||||
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
||||||
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
||||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
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'),
|
destroyMainOverlayWindow: () => calls.push('destroy-main-overlay-window'),
|
||||||
destroyModalOverlayWindow: () => calls.push('destroy-modal-overlay-window'),
|
destroyModalOverlayWindow: () => calls.push('destroy-modal-overlay-window'),
|
||||||
destroyYomitanParserWindow: () => calls.push('destroy-yomitan-window'),
|
destroyYomitanParserWindow: () => calls.push('destroy-yomitan-window'),
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ export function buildConfigHotReloadPayload(config: ResolvedConfig): ConfigHotRe
|
|||||||
shortcuts: resolveConfiguredShortcuts(config, DEFAULT_CONFIG),
|
shortcuts: resolveConfiguredShortcuts(config, DEFAULT_CONFIG),
|
||||||
statsToggleKey: config.stats.toggleKey,
|
statsToggleKey: config.stats.toggleKey,
|
||||||
platform:
|
platform:
|
||||||
process.platform === 'darwin'
|
process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'win32' : 'linux',
|
||||||
? 'darwin'
|
|
||||||
: process.platform === 'win32'
|
|
||||||
? 'win32'
|
|
||||||
: 'linux',
|
|
||||||
rawConfig: config,
|
rawConfig: config,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -94,10 +94,7 @@ test('shouldAutoOpenFirstRunSetup only for startup/setup intents', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('shouldAutoOpenFirstRunSetup treats numeric startup counts as explicit commands', () => {
|
test('shouldAutoOpenFirstRunSetup treats numeric startup counts as explicit commands', () => {
|
||||||
assert.equal(
|
assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ start: true, copySubtitleCount: 2 })), false);
|
||||||
shouldAutoOpenFirstRunSetup(makeArgs({ start: true, copySubtitleCount: 2 })),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldAutoOpenFirstRunSetup(makeArgs({ background: true, mineSentenceCount: 1 })),
|
shouldAutoOpenFirstRunSetup(makeArgs({ background: true, mineSentenceCount: 1 })),
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ test('createImmersionTrackerStartupHandler keeps tracker startup alive when mpv
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.equal(
|
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,
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ export function createImmersionTrackerStartupHandler(
|
|||||||
try {
|
try {
|
||||||
mpvClient.connect();
|
mpvClient.connect();
|
||||||
} catch (error) {
|
} 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();
|
deps.seedTrackerFromCurrentMedia();
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ function createQueuedIpcListenerWithPayload<T>(
|
|||||||
|
|
||||||
const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen);
|
const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen);
|
||||||
const onOpenSessionHelpEvent = createQueuedIpcListener(IPC_CHANNELS.event.sessionHelpOpen);
|
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 onOpenControllerDebugEvent = createQueuedIpcListener(IPC_CHANNELS.event.controllerDebugOpen);
|
||||||
const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen);
|
const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen);
|
||||||
const onOpenYoutubeTrackPickerEvent = createQueuedIpcListenerWithPayload<YoutubePickerOpenPayload>(
|
const onOpenYoutubeTrackPickerEvent = createQueuedIpcListenerWithPayload<YoutubePickerOpenPayload>(
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ test('prerelease workflow writes checksum entries using release asset basenames'
|
|||||||
assert.match(prereleaseWorkflow, /: > release\/SHA256SUMS\.txt/);
|
assert.match(prereleaseWorkflow, /: > release\/SHA256SUMS\.txt/);
|
||||||
assert.match(prereleaseWorkflow, /for file in "\$\{files\[@\]\}"; do/);
|
assert.match(prereleaseWorkflow, /for file in "\$\{files\[@\]\}"; do/);
|
||||||
assert.match(prereleaseWorkflow, /\$\{file##\*\/\}/);
|
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', () => {
|
test('prerelease workflow validates artifacts before publishing the release and only undrafts after upload', () => {
|
||||||
|
|||||||
@@ -364,7 +364,10 @@ test('isYomitanPopupVisible requires visible iframe geometry', () => {
|
|||||||
const root = {
|
const root = {
|
||||||
querySelectorAll: (value: string) => {
|
querySelectorAll: (value: string) => {
|
||||||
selectors.push(value);
|
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 [];
|
||||||
}
|
}
|
||||||
return [hiddenFrame, visibleFrame];
|
return [hiddenFrame, visibleFrame];
|
||||||
|
|||||||
@@ -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 });
|
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 {
|
} finally {
|
||||||
testGlobals.restore();
|
testGlobals.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,10 @@ export function createKeyboardHandlers(
|
|||||||
let pendingLookupRefreshAfterSubtitleSeek = false;
|
let pendingLookupRefreshAfterSubtitleSeek = false;
|
||||||
let resetSelectionToStartOnNextSubtitleSync = false;
|
let resetSelectionToStartOnNextSubtitleSync = false;
|
||||||
let lookupScanFallbackTimer: ReturnType<typeof setTimeout> | null = null;
|
let lookupScanFallbackTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let pendingNumericSelection:
|
let pendingNumericSelection: {
|
||||||
| {
|
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
|
||||||
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
|
timeout: ReturnType<typeof setTimeout> | null;
|
||||||
timeout: ReturnType<typeof setTimeout> | null;
|
} | null = null;
|
||||||
}
|
|
||||||
| null = null;
|
|
||||||
|
|
||||||
const CHORD_MAP = new Map<
|
const CHORD_MAP = new Map<
|
||||||
string,
|
string,
|
||||||
|
|||||||
@@ -73,11 +73,13 @@ export function createMouseHandlers(
|
|||||||
syncOverlayMouseIgnoreState(ctx);
|
syncOverlayMouseIgnoreState(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reconcilePopupInteraction(args: {
|
function reconcilePopupInteraction(
|
||||||
assumeVisible?: boolean;
|
args: {
|
||||||
reclaimFocus?: boolean;
|
assumeVisible?: boolean;
|
||||||
allowPause?: boolean;
|
reclaimFocus?: boolean;
|
||||||
} = {}): boolean {
|
allowPause?: boolean;
|
||||||
|
} = {},
|
||||||
|
): boolean {
|
||||||
const popupVisible = syncPopupVisibilityState(args.assumeVisible === true);
|
const popupVisible = syncPopupVisibilityState(args.assumeVisible === true);
|
||||||
if (!popupVisible) {
|
if (!popupVisible) {
|
||||||
syncOverlayMouseIgnoreState(ctx);
|
syncOverlayMouseIgnoreState(ctx);
|
||||||
|
|||||||
@@ -168,48 +168,54 @@ function withRuntimeOptionsModal(
|
|||||||
test('openRuntimeOptionsModal shows loading shell before runtime options resolve', async () => {
|
test('openRuntimeOptionsModal shows loading shell before runtime options resolve', async () => {
|
||||||
const deferred = createDeferred<RuntimeOptionState[]>();
|
const deferred = createDeferred<RuntimeOptionState[]>();
|
||||||
|
|
||||||
await withRuntimeOptionsModal(() => deferred.promise, async (input) => {
|
await withRuntimeOptionsModal(
|
||||||
input.modal.openRuntimeOptionsModal();
|
() => deferred.promise,
|
||||||
|
async (input) => {
|
||||||
|
input.modal.openRuntimeOptionsModal();
|
||||||
|
|
||||||
assert.equal(input.state.runtimeOptionsModalOpen, true);
|
assert.equal(input.state.runtimeOptionsModalOpen, true);
|
||||||
assert.equal(input.overlayClassList.contains('interactive'), true);
|
assert.equal(input.overlayClassList.contains('interactive'), true);
|
||||||
assert.equal(input.modalClassList.contains('hidden'), false);
|
assert.equal(input.modalClassList.contains('hidden'), false);
|
||||||
assert.equal(input.statusNode.textContent, 'Loading runtime options...');
|
assert.equal(input.statusNode.textContent, 'Loading runtime options...');
|
||||||
assert.deepEqual(input.syncCalls, ['sync']);
|
assert.deepEqual(input.syncCalls, ['sync']);
|
||||||
|
|
||||||
deferred.resolve([
|
deferred.resolve([
|
||||||
{
|
{
|
||||||
id: 'anki.autoUpdateNewCards',
|
id: 'anki.autoUpdateNewCards',
|
||||||
label: 'Auto-update new cards',
|
label: 'Auto-update new cards',
|
||||||
scope: 'ankiConnect',
|
scope: 'ankiConnect',
|
||||||
valueType: 'boolean',
|
valueType: 'boolean',
|
||||||
value: true,
|
value: true,
|
||||||
allowedValues: [true, false],
|
allowedValues: [true, false],
|
||||||
requiresRestart: false,
|
requiresRestart: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
await flushAsyncWork();
|
await flushAsyncWork();
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
input.statusNode.textContent,
|
input.statusNode.textContent,
|
||||||
'Use arrow keys. Click value to cycle. Enter or double-click to apply.',
|
'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.classList.contains('error'), false);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('openRuntimeOptionsModal keeps modal visible when loading fails', async () => {
|
test('openRuntimeOptionsModal keeps modal visible when loading fails', async () => {
|
||||||
const deferred = createDeferred<RuntimeOptionState[]>();
|
const deferred = createDeferred<RuntimeOptionState[]>();
|
||||||
|
|
||||||
await withRuntimeOptionsModal(() => deferred.promise, async (input) => {
|
await withRuntimeOptionsModal(
|
||||||
input.modal.openRuntimeOptionsModal();
|
() => deferred.promise,
|
||||||
deferred.reject(new Error('boom'));
|
async (input) => {
|
||||||
await flushAsyncWork();
|
input.modal.openRuntimeOptionsModal();
|
||||||
|
deferred.reject(new Error('boom'));
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
assert.equal(input.state.runtimeOptionsModalOpen, true);
|
assert.equal(input.state.runtimeOptionsModalOpen, true);
|
||||||
assert.equal(input.overlayClassList.contains('interactive'), true);
|
assert.equal(input.overlayClassList.contains('interactive'), true);
|
||||||
assert.equal(input.modalClassList.contains('hidden'), false);
|
assert.equal(input.modalClassList.contains('hidden'), false);
|
||||||
assert.equal(input.statusNode.textContent, 'Failed to load runtime options');
|
assert.equal(input.statusNode.textContent, 'Failed to load runtime options');
|
||||||
assert.equal(input.statusNode.classList.contains('error'), true);
|
assert.equal(input.statusNode.classList.contains('error'), true);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ test('visible yomitan popup host keeps overlay interactive even when cached popu
|
|||||||
},
|
},
|
||||||
document: {
|
document: {
|
||||||
querySelectorAll: (selector: string) =>
|
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' }]
|
? [{ getAttribute: () => 'true' }]
|
||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ function queryPopupElements<T extends Element>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isYomitanPopupVisible(root: ParentNode | null | undefined = document): boolean {
|
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) {
|
if (visiblePopupHosts.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,7 +256,9 @@ export function parseSessionActionDispatchRequest(
|
|||||||
|
|
||||||
const payload = parseSessionActionPayload(value.actionId, value.payload);
|
const payload = parseSessionActionPayload(value.actionId, value.payload);
|
||||||
if (payload === null) return null;
|
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 {
|
export function parseMpvCommand(value: unknown): Array<string | number> | null {
|
||||||
|
|||||||
@@ -364,7 +364,10 @@ export interface ElectronAPI {
|
|||||||
getKeybindings: () => Promise<Keybinding[]>;
|
getKeybindings: () => Promise<Keybinding[]>;
|
||||||
getSessionBindings: () => Promise<CompiledSessionBinding[]>;
|
getSessionBindings: () => Promise<CompiledSessionBinding[]>;
|
||||||
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
|
getConfiguredShortcuts: () => Promise<Required<ShortcutsConfig>>;
|
||||||
dispatchSessionAction: (actionId: SessionActionId, payload?: SessionActionPayload) => Promise<void>;
|
dispatchSessionAction: (
|
||||||
|
actionId: SessionActionId,
|
||||||
|
payload?: SessionActionPayload,
|
||||||
|
) => Promise<void>;
|
||||||
getStatsToggleKey: () => Promise<string>;
|
getStatsToggleKey: () => Promise<string>;
|
||||||
getMarkWatchedKey: () => Promise<string>;
|
getMarkWatchedKey: () => Promise<string>;
|
||||||
markActiveVideoWatched: () => Promise<boolean>;
|
markActiveVideoWatched: () => Promise<boolean>;
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ export interface CompiledSessionActionBinding extends CompiledSessionBindingBase
|
|||||||
payload?: SessionActionPayload;
|
payload?: SessionActionPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompiledSessionBinding =
|
export type CompiledSessionBinding = CompiledMpvCommandBinding | CompiledSessionActionBinding;
|
||||||
| CompiledMpvCommandBinding
|
|
||||||
| CompiledSessionActionBinding;
|
|
||||||
|
|
||||||
export interface PluginSessionBindingsArtifact {
|
export interface PluginSessionBindingsArtifact {
|
||||||
version: 1;
|
version: 1;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
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';
|
import type { MpvPollResult } from './win32';
|
||||||
|
|
||||||
function createPollResult(commandLines: Array<string | null>): MpvPollResult {
|
function createPollResult(commandLines: Array<string | null>): MpvPollResult {
|
||||||
@@ -51,7 +54,10 @@ test('filterMpvPollResultBySocketPath keeps only matches for the requested socke
|
|||||||
'\\\\.\\pipe\\subminer-b',
|
'\\\\.\\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');
|
assert.equal(result.windowState, 'visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export function findWindowsMpvTargetWindowHandle(result?: MpvPollResult): number
|
|||||||
const poll = result ?? loadWin32().findMpvWindows();
|
const poll = result ?? loadWin32().findMpvWindows();
|
||||||
const focused = poll.matches.find((match) => match.isForeground);
|
const focused = poll.matches.find((match) => match.isForeground);
|
||||||
const best =
|
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;
|
return best?.hwnd ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { WindowsWindowTracker } from './windows-tracker';
|
|||||||
import type { MpvPollResult } from './win32';
|
import type { MpvPollResult } from './win32';
|
||||||
|
|
||||||
function mpvVisible(
|
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 {
|
): MpvPollResult {
|
||||||
return {
|
return {
|
||||||
matches: [
|
matches: [
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ export class WindowsWindowTracker extends BaseWindowTracker {
|
|||||||
constructor(_targetMpvSocketPath?: string, deps: WindowsTrackerDeps = {}) {
|
constructor(_targetMpvSocketPath?: string, deps: WindowsTrackerDeps = {}) {
|
||||||
super();
|
super();
|
||||||
this.targetMpvSocketPath = _targetMpvSocketPath?.trim() || null;
|
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.maxConsecutiveMisses = Math.max(1, Math.floor(deps.maxConsecutiveMisses ?? 2));
|
||||||
this.trackingLossGraceMs = Math.max(0, Math.floor(deps.trackingLossGraceMs ?? 1_500));
|
this.trackingLossGraceMs = Math.max(0, Math.floor(deps.trackingLossGraceMs ?? 1_500));
|
||||||
this.minimizedTrackingLossGraceMs = Math.max(
|
this.minimizedTrackingLossGraceMs = Math.max(
|
||||||
|
|||||||
Reference in New Issue
Block a user