diff --git a/changes/character-dictionary-docs.md b/changes/character-dictionary-docs.md deleted file mode 100644 index 66eda5cd..00000000 --- a/changes/character-dictionary-docs.md +++ /dev/null @@ -1,6 +0,0 @@ -type: docs -area: character-dictionary - -- Corrected character dictionary setup docs: AniList authentication is not required; the feature uses public GraphQL queries and only needs `subtitleStyle.nameMatchEnabled`. -- Added documentation for inline character portraits (`subtitleStyle.nameMatchImagesEnabled`). -- Clarified that AniList authentication is only needed for watch-progress sync, not the character dictionary. diff --git a/changes/macos-autoplay-pointer-recovery.md b/changes/macos-autoplay-pointer-recovery.md new file mode 100644 index 00000000..f306af83 --- /dev/null +++ b/changes/macos-autoplay-pointer-recovery.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- Fixed the macOS visible subtitle overlay staying click-through after pause-until-ready releases playback. diff --git a/changes/macos-modal-focus.md b/changes/macos-modal-focus.md new file mode 100644 index 00000000..0d60dc75 --- /dev/null +++ b/changes/macos-modal-focus.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- Restored mpv focus after closing dedicated modal windows on macOS so subtitles and playback keybinds resume without clicking the player. diff --git a/changes/mpv-plugin-multicopy-selector.md b/changes/mpv-plugin-multicopy-selector.md deleted file mode 100644 index ffb90c55..00000000 --- a/changes/mpv-plugin-multicopy-selector.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Fixed mpv-plugin multi-line copy and mine shortcuts so they open the overlay digit selector instead of dispatching a missing-count action that immediately selects one line. diff --git a/changes/overlay-pause-hover-live-state.md b/changes/overlay-pause-hover-live-state.md deleted file mode 100644 index 27ac2ef0..00000000 --- a/changes/overlay-pause-hover-live-state.md +++ /dev/null @@ -1,4 +0,0 @@ -type: fixed -area: overlay - -- Fixed subtitle hover auto-pause using stale pause state, which could briefly advance a paused mpv video on Linux/X11 or XWayland. diff --git a/package.json b/package.json index 6a604790..7c8a394c 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "test:plugin:src": "lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-session-bindings.lua && lua scripts/test-plugin-binary-windows.lua", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src", - "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/shared/mpv-x11-backend.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/settings-window-z-order.test.ts src/core/services/hyprland-window-placement.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-manager.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/stats-window-lifecycle.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-ready-gate.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/main/runtime/visible-overlay-autoplay-readiness.test.ts src/main/runtime/character-dictionary-manager-gate.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/core/utils/electron-backend.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/linux-overlay-pointer-interaction.test.ts src/main/runtime/linux-overlay-zorder-keepalive.test.ts src/main/runtime/config-settings-window.test.ts src/main/runtime/settings-window-z-order.test.ts src/main/runtime/setup-window-factory.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/log-export.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/overlay-content-measurement.test.ts src/renderer/subtitle-render.test.ts src/renderer/subtitle-render-word-class.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/main/character-dictionary-runtime/term-building.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts src/core/services/overlay-visibility.test.ts src/core/services/overlay-window-config.test.ts src/core/services/overlay-window.test.ts src/main/main-wiring.test.ts src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts src/main/runtime/mpv-main-event-actions.test.ts src/main/runtime/overlay-modal-input-state.test.ts src/main/runtime/overlay-window-factory-main-deps.test.ts src/main/runtime/overlay-window-factory.test.ts src/main/runtime/overlay-window-layout-main-deps.test.ts src/main/runtime/overlay-window-layout.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts src/main/runtime/yomitan-extension-overlay-reload.test.ts src/renderer/modals/subtitle-sidebar.test.ts src/renderer/overlay-mouse-ignore.test.ts src/main/runtime/linux-visible-overlay-window-mode.test.ts src/main/runtime/linux-x11-cursor-point.test.ts src/renderer/renderer-init-order.test.ts", - "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/shared/setup-state.test.js dist/shared/mpv-x11-backend.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/yomitan-extension-loader.test.js dist/core/services/yomitan-settings.test.js dist/core/services/settings-window-z-order.test.js dist/core/services/hyprland-window-placement.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/stats-window.test.js dist/core/services/stats-window-lifecycle.test.js dist/core/services/__tests__/stats-server.test.js dist/main/runtime/stats-server-routing.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/autoplay-ready-gate.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/main/runtime/visible-overlay-autoplay-readiness.test.js dist/main/runtime/character-dictionary-manager-gate.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/core/utils/shortcut-config.test.js dist/core/utils/electron-backend.test.js dist/main/runtime/startup-mode-flags.test.js dist/main/runtime/linux-overlay-pointer-interaction.test.js dist/main/runtime/linux-overlay-zorder-keepalive.test.js dist/main/runtime/config-settings-window.test.js dist/main/runtime/settings-window-z-order.test.js dist/main/runtime/setup-window-factory.test.js dist/main/runtime/first-run-setup-plugin.test.js dist/main/runtime/first-run-setup-service.test.js dist/main/runtime/first-run-setup-window.test.js dist/main/runtime/command-line-launcher.test.js dist/main/runtime/log-export.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/tray-main-actions.test.js dist/main/runtime/tray-main-deps.test.js dist/main/runtime/tray-runtime-handlers.test.js dist/main/runtime/cli-command-context-main-deps.test.js dist/main/runtime/app-ready-main-deps.test.js dist/main/runtime/update/appimage-updater.test.js dist/main/runtime/update/fetch-adapter.test.js dist/main/runtime/update/release-metadata-policy.test.js dist/main/runtime/update/update-dialogs.test.js dist/main/runtime/update/support-assets.test.js dist/renderer/error-recovery.test.js dist/renderer/overlay-content-measurement.test.js dist/renderer/subtitle-render.test.js dist/renderer/subtitle-render-word-class.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/main/character-dictionary-runtime/term-building.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js dist/core/services/overlay-visibility.test.js dist/core/services/overlay-window-config.test.js dist/core/services/overlay-window.test.js dist/main/main-wiring.test.js dist/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.js dist/main/runtime/mpv-main-event-actions.test.js dist/main/runtime/overlay-modal-input-state.test.js dist/main/runtime/overlay-window-factory-main-deps.test.js dist/main/runtime/overlay-window-factory.test.js dist/main/runtime/overlay-window-layout-main-deps.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/overlay-window-runtime-handlers.test.js dist/main/runtime/yomitan-extension-overlay-reload.test.js dist/renderer/modals/subtitle-sidebar.test.js dist/renderer/overlay-mouse-ignore.test.js dist/main/runtime/linux-visible-overlay-window-mode.test.js dist/main/runtime/linux-x11-cursor-point.test.js dist/renderer/renderer-init-order.test.js", + "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/settings/settings-model.test.ts src/settings/settings-field-layout.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/shared/mpv-x11-backend.test.ts src/core/services/cli-command.test.ts src/core/services/ipc.test.ts src/core/services/anki-jimaku-ipc.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/settings-window-z-order.test.ts src/core/services/hyprland-window-placement.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-manager.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/stats-window-lifecycle.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/overlay-runtime.test.ts src/main/runtime/macos-mpv-focus.test.ts src/main/runtime/macos-modal-focus-handoff.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/main/runtime/autoplay-ready-gate.test.ts src/main/runtime/autoplay-tokenization-warm-release.test.ts src/main/runtime/autoplay-subtitle-primer.test.ts src/main/runtime/visible-overlay-autoplay-readiness.test.ts src/main/runtime/character-dictionary-manager-gate.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/core/utils/electron-backend.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/linux-overlay-pointer-interaction.test.ts src/main/runtime/linux-overlay-zorder-keepalive.test.ts src/main/runtime/config-settings-window.test.ts src/main/runtime/settings-window-z-order.test.ts src/main/runtime/setup-window-factory.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/log-export.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/overlay-content-measurement.test.ts src/renderer/subtitle-render.test.ts src/renderer/subtitle-render-word-class.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/main/character-dictionary-runtime/term-building.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts src/core/services/overlay-visibility.test.ts src/core/services/overlay-window-config.test.ts src/core/services/overlay-window.test.ts src/main/main-wiring.test.ts src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts src/main/runtime/mpv-main-event-actions.test.ts src/main/runtime/overlay-modal-input-state.test.ts src/main/runtime/overlay-window-factory-main-deps.test.ts src/main/runtime/overlay-window-factory.test.ts src/main/runtime/overlay-window-layout-main-deps.test.ts src/main/runtime/overlay-window-layout.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts src/main/runtime/yomitan-extension-overlay-reload.test.ts src/renderer/modals/subtitle-sidebar.test.ts src/renderer/overlay-mouse-ignore.test.ts src/main/runtime/linux-visible-overlay-window-mode.test.ts src/main/runtime/linux-x11-cursor-point.test.ts src/renderer/renderer-init-order.test.ts", + "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/settings/settings-model.test.js dist/settings/settings-field-layout.test.js dist/cli/args.test.js dist/cli/help.test.js dist/shared/setup-state.test.js dist/shared/mpv-x11-backend.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/yomitan-extension-loader.test.js dist/core/services/yomitan-settings.test.js dist/core/services/settings-window-z-order.test.js dist/core/services/hyprland-window-placement.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/stats-window.test.js dist/core/services/stats-window-lifecycle.test.js dist/core/services/__tests__/stats-server.test.js dist/main/runtime/stats-server-routing.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/overlay-runtime.test.js dist/main/runtime/macos-mpv-focus.test.js dist/main/runtime/macos-modal-focus-handoff.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/main/runtime/autoplay-ready-gate.test.js dist/main/runtime/autoplay-tokenization-warm-release.test.js dist/main/runtime/autoplay-subtitle-primer.test.js dist/main/runtime/visible-overlay-autoplay-readiness.test.js dist/main/runtime/character-dictionary-manager-gate.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/core/utils/shortcut-config.test.js dist/core/utils/electron-backend.test.js dist/main/runtime/startup-mode-flags.test.js dist/main/runtime/linux-overlay-pointer-interaction.test.js dist/main/runtime/linux-overlay-zorder-keepalive.test.js dist/main/runtime/config-settings-window.test.js dist/main/runtime/settings-window-z-order.test.js dist/main/runtime/setup-window-factory.test.js dist/main/runtime/first-run-setup-plugin.test.js dist/main/runtime/first-run-setup-service.test.js dist/main/runtime/first-run-setup-window.test.js dist/main/runtime/command-line-launcher.test.js dist/main/runtime/log-export.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/tray-main-actions.test.js dist/main/runtime/tray-main-deps.test.js dist/main/runtime/tray-runtime-handlers.test.js dist/main/runtime/cli-command-context-main-deps.test.js dist/main/runtime/app-ready-main-deps.test.js dist/main/runtime/update/appimage-updater.test.js dist/main/runtime/update/fetch-adapter.test.js dist/main/runtime/update/release-metadata-policy.test.js dist/main/runtime/update/update-dialogs.test.js dist/main/runtime/update/support-assets.test.js dist/renderer/error-recovery.test.js dist/renderer/overlay-content-measurement.test.js dist/renderer/subtitle-render.test.js dist/renderer/subtitle-render-word-class.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/main/character-dictionary-runtime/term-building.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js dist/core/services/overlay-visibility.test.js dist/core/services/overlay-window-config.test.js dist/core/services/overlay-window.test.js dist/main/main-wiring.test.js dist/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.js dist/main/runtime/mpv-main-event-actions.test.js dist/main/runtime/overlay-modal-input-state.test.js dist/main/runtime/overlay-window-factory-main-deps.test.js dist/main/runtime/overlay-window-factory.test.js dist/main/runtime/overlay-window-layout-main-deps.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/overlay-window-runtime-handlers.test.js dist/main/runtime/yomitan-extension-overlay-reload.test.js dist/renderer/modals/subtitle-sidebar.test.js dist/renderer/overlay-mouse-ignore.test.js dist/main/runtime/linux-visible-overlay-window-mode.test.js dist/main/runtime/linux-x11-cursor-point.test.js dist/renderer/renderer-init-order.test.js", "test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js", "test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist", "test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts", diff --git a/src/main.ts b/src/main.ts index 77f81d43..06511b62 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1255,6 +1255,12 @@ const autoplayReadyGate = createAutoplayReadyGate({ signalPluginAutoplayReady: () => { sendMpvCommandRuntime(appState.mpvClient, ['script-message', 'subminer-autoplay-ready']); }, + requestOverlayPointerRecovery: () => { + if (process.platform !== 'darwin' || !overlayManager.getVisibleOverlayVisible()) { + return; + } + broadcastToOverlayWindows(IPC_CHANNELS.event.overlayPointerRecoveryRequest); + }, isSignalTargetReady: (signal) => isTokenizationWarmupReady() && isVisibleOverlayAutoplayTargetReady( diff --git a/src/main/boot/services.ts b/src/main/boot/services.ts index dd80ec7a..b29753ee 100644 --- a/src/main/boot/services.ts +++ b/src/main/boot/services.ts @@ -219,6 +219,7 @@ export function createMainBootServices< params.getSyncOverlayVisibilityForModal()(); }, restoreMainWindowFocus: () => { + if (params.platform === 'darwin') return; const mainWindow = overlayManager.getMainWindow(); if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) return; try { diff --git a/src/main/overlay-runtime.test.ts b/src/main/overlay-runtime.test.ts index ad7a52f9..9841a849 100644 --- a/src/main/overlay-runtime.test.ts +++ b/src/main/overlay-runtime.test.ts @@ -417,17 +417,25 @@ test('modal window path makes visible main overlay click-through until modal clo assert.equal(mainWindow.ignoreMouseEvents, true); }); -test('modal window path hides visible main overlay until modal closes', () => { +test('modal window path restores visible main overlay before modal input deactivates', () => { const mainWindow = createMockWindow(); mainWindow.visible = true; const modalWindow = createMockWindow(); - const runtime = createOverlayModalRuntimeService({ - getMainWindow: () => mainWindow as never, - getModalWindow: () => modalWindow as never, - createModalWindow: () => modalWindow as never, - getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), - setModalWindowBounds: () => {}, - }); + const events: string[] = []; + const runtime = createOverlayModalRuntimeService( + { + getMainWindow: () => mainWindow as never, + getModalWindow: () => modalWindow as never, + createModalWindow: () => modalWindow as never, + getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), + setModalWindowBounds: () => {}, + }, + { + onModalStateChange: (active: boolean): void => { + events.push(`state:${active}:visible:${mainWindow.isVisible()}`); + }, + }, + ); runtime.sendToActiveOverlayWindow( 'youtube:picker-open', @@ -444,8 +452,88 @@ test('modal window path hides visible main overlay until modal closes', () => { runtime.handleOverlayModalClosed('youtube-track-picker'); - assert.equal(mainWindow.getShowCount(), 0); - assert.equal(mainWindow.isVisible(), false); + assert.equal(mainWindow.getShowCount(), 1); + assert.equal(mainWindow.isVisible(), true); + assert.deepEqual(events, ['state:true:visible:true', 'state:false:visible:true']); +}); + +test('modal window path runs final close handoff before modal input deactivates', () => { + const mainWindow = createMockWindow(); + mainWindow.visible = true; + const modalWindow = createMockWindow(); + const events: string[] = []; + const runtime = createOverlayModalRuntimeService( + { + getMainWindow: () => mainWindow as never, + getModalWindow: () => modalWindow as never, + createModalWindow: () => modalWindow as never, + getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), + setModalWindowBounds: () => {}, + }, + { + onFinalModalClosed: (): void => { + events.push(`handoff:visible:${mainWindow.isVisible()}`); + }, + onModalStateChange: (active: boolean): void => { + events.push(`state:${active}:visible:${mainWindow.isVisible()}`); + }, + }, + ); + + runtime.sendToActiveOverlayWindow( + 'youtube:picker-open', + { sessionId: 'yt-1' }, + { + restoreOnModalClose: 'youtube-track-picker', + preferModalWindow: true, + }, + ); + runtime.notifyOverlayModalOpened('youtube-track-picker'); + runtime.handleOverlayModalClosed('youtube-track-picker'); + + assert.deepEqual(events, [ + 'state:true:visible:true', + 'handoff:visible:true', + 'state:false:visible:true', + ]); +}); + +test('modal runtime deactivates modal state when final close handoff throws', () => { + const mainWindow = createMockWindow(); + mainWindow.visible = true; + const modalWindow = createMockWindow(); + const events: string[] = []; + const runtime = createOverlayModalRuntimeService( + { + getMainWindow: () => mainWindow as never, + getModalWindow: () => modalWindow as never, + createModalWindow: () => modalWindow as never, + getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }), + setModalWindowBounds: () => {}, + }, + { + onFinalModalClosed: (): void => { + events.push('handoff'); + throw new Error('handoff failed'); + }, + onModalStateChange: (active: boolean): void => { + events.push(`state:${active}`); + }, + }, + ); + + runtime.sendToActiveOverlayWindow( + 'youtube:picker-open', + { sessionId: 'yt-1' }, + { + restoreOnModalClose: 'youtube-track-picker', + preferModalWindow: true, + }, + ); + runtime.notifyOverlayModalOpened('youtube-track-picker'); + + assert.doesNotThrow(() => runtime.handleOverlayModalClosed('youtube-track-picker')); + assert.deepEqual(events, ['state:true', 'handoff', 'state:false']); }); test('modal runtime notifies callers when modal input state becomes active/inactive', () => { diff --git a/src/main/overlay-runtime.ts b/src/main/overlay-runtime.ts index b9b2401d..35978804 100644 --- a/src/main/overlay-runtime.ts +++ b/src/main/overlay-runtime.ts @@ -54,6 +54,7 @@ type RevealFallbackHandle = NonNullable void; + onFinalModalClosed?: () => void; scheduleRevealFallback?: (callback: () => void, delayMs: number) => RevealFallbackHandle; clearRevealFallback?: (timeout: RevealFallbackHandle) => void; } @@ -387,8 +388,14 @@ export function createOverlayModalRuntimeService( } modalWindowPrimedForImmediateShow = false; mainWindowMousePassthroughForcedByModal = false; - mainWindowHiddenByModal = false; - notifyModalStateChange(false); + setMainWindowVisibilityForModal(false); + try { + options.onFinalModalClosed?.(); + } catch { + // Modal state still needs to deactivate if focus handoff fails. + } finally { + notifyModalStateChange(false); + } } }; diff --git a/src/main/runtime/autoplay-ready-gate.test.ts b/src/main/runtime/autoplay-ready-gate.test.ts index 998b967d..af25162c 100644 --- a/src/main/runtime/autoplay-ready-gate.test.ts +++ b/src/main/runtime/autoplay-ready-gate.test.ts @@ -95,6 +95,48 @@ test('autoplay ready gate retry loop does not re-signal plugin readiness', async ); }); +test('autoplay ready gate requests overlay pointer recovery when media readiness is signaled', async () => { + const commands: Array> = []; + let pointerRecoveryRequests = 0; + + const gate = createAutoplayReadyGate({ + isAppOwnedFlowInFlight: () => false, + getCurrentMediaPath: () => '/media/video.mkv', + getCurrentVideoPath: () => null, + getPlaybackPaused: () => true, + getMpvClient: () => + ({ + connected: true, + requestProperty: async () => true, + send: ({ command }: { command: Array }) => { + commands.push(command); + }, + }) as never, + signalPluginAutoplayReady: () => { + commands.push(['script-message', 'subminer-autoplay-ready']); + }, + requestOverlayPointerRecovery: () => { + pointerRecoveryRequests += 1; + }, + schedule: (callback) => { + queueMicrotask(callback); + return 1 as never; + }, + logDebug: () => {}, + }); + + gate.maybeSignalPluginAutoplayReady({ text: '字幕', tokens: null }, { forceWhilePaused: true }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + gate.maybeSignalPluginAutoplayReady( + { text: '字幕その2', tokens: null }, + { forceWhilePaused: true }, + ); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.equal(pointerRecoveryRequests, 1); +}); + test('autoplay ready gate does not unpause again after a later manual pause on the same media', async () => { const commands: Array> = []; let playbackPaused = true; diff --git a/src/main/runtime/autoplay-ready-gate.ts b/src/main/runtime/autoplay-ready-gate.ts index 2e7f38af..f9c49bf8 100644 --- a/src/main/runtime/autoplay-ready-gate.ts +++ b/src/main/runtime/autoplay-ready-gate.ts @@ -23,6 +23,7 @@ export type AutoplayReadyGateDeps = { getPlaybackPaused: () => boolean | null; getMpvClient: () => MpvClientLike | null; signalPluginAutoplayReady: () => void; + requestOverlayPointerRecovery?: () => void; isSignalTargetReady?: (signal: AutoplayReadySignal) => boolean; now?: () => number; schedule: (callback: () => void, delayMs: number) => ReturnType; @@ -141,6 +142,7 @@ export function createAutoplayReadyGate(deps: AutoplayReadyGateDeps) { autoPlayReadySignalMediaPath = mediaPath; const playbackGeneration = ++autoPlayReadySignalGeneration; deps.signalPluginAutoplayReady(); + deps.requestOverlayPointerRecovery?.(); attemptRelease(playbackGeneration, 0); }; diff --git a/src/main/runtime/macos-modal-focus-handoff.test.ts b/src/main/runtime/macos-modal-focus-handoff.test.ts new file mode 100644 index 00000000..ec5c802c --- /dev/null +++ b/src/main/runtime/macos-modal-focus-handoff.test.ts @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { restoreMacOSMpvFocusAfterModalClose } from './macos-modal-focus-handoff'; + +test('restoreMacOSMpvFocusAfterModalClose focuses mpv, refreshes tracker, then updates visibility on macOS', async () => { + const calls: string[] = []; + + await restoreMacOSMpvFocusAfterModalClose({ + platform: 'darwin', + focusMpv: async () => { + calls.push('focus'); + }, + getWindowTracker: () => ({ + refreshNow: async () => { + calls.push('refresh'); + }, + }), + updateVisibleOverlayVisibility: () => { + calls.push('visibility'); + }, + warn: () => {}, + }); + + assert.deepEqual(calls, ['focus', 'refresh', 'visibility']); +}); + +test('restoreMacOSMpvFocusAfterModalClose skips non-macOS platforms', async () => { + const calls: string[] = []; + + await restoreMacOSMpvFocusAfterModalClose({ + platform: 'linux', + focusMpv: async () => { + calls.push('focus'); + }, + getWindowTracker: () => null, + updateVisibleOverlayVisibility: () => { + calls.push('visibility'); + }, + warn: () => {}, + }); + + assert.deepEqual(calls, []); +}); + +test('restoreMacOSMpvFocusAfterModalClose still updates visibility when tracker refresh fails', async () => { + const calls: string[] = []; + + await restoreMacOSMpvFocusAfterModalClose({ + platform: 'darwin', + focusMpv: async () => { + calls.push('focus'); + }, + getWindowTracker: () => ({ + refreshNow: async () => { + calls.push('refresh'); + throw new Error('refresh failed'); + }, + }), + updateVisibleOverlayVisibility: () => { + calls.push('visibility'); + }, + warn: (message) => { + calls.push(`warn:${message}`); + }, + }); + + assert.deepEqual(calls, [ + 'focus', + 'refresh', + 'warn:Failed to refresh macOS mpv focus after modal close', + 'visibility', + ]); +}); diff --git a/src/main/runtime/macos-modal-focus-handoff.ts b/src/main/runtime/macos-modal-focus-handoff.ts new file mode 100644 index 00000000..9478fddf --- /dev/null +++ b/src/main/runtime/macos-modal-focus-handoff.ts @@ -0,0 +1,33 @@ +type RefreshableWindowTracker = { + refreshNow: () => Promise; +}; + +export type MacOSModalFocusHandoffDeps = { + platform: NodeJS.Platform; + focusMpv: () => Promise; + getWindowTracker: () => RefreshableWindowTracker | null; + updateVisibleOverlayVisibility: () => void; + warn: (message: string, details?: unknown) => void; +}; + +export async function restoreMacOSMpvFocusAfterModalClose( + deps: MacOSModalFocusHandoffDeps, +): Promise { + if (deps.platform !== 'darwin') { + return; + } + + try { + await deps.focusMpv(); + } catch (error) { + deps.warn('Failed to focus mpv after macOS modal close', error); + } + + try { + await deps.getWindowTracker()?.refreshNow(); + } catch (error) { + deps.warn('Failed to refresh macOS mpv focus after modal close', error); + } + + deps.updateVisibleOverlayVisibility(); +} diff --git a/src/main/runtime/macos-mpv-focus.test.ts b/src/main/runtime/macos-mpv-focus.test.ts new file mode 100644 index 00000000..aeb12770 --- /dev/null +++ b/src/main/runtime/macos-mpv-focus.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { focusMacOSMpvProcess } from './macos-mpv-focus'; + +test('focusMacOSMpvProcess fronts the running mpv process with osascript', async () => { + const calls: Array<{ command: string; args: string[]; timeout?: number }> = []; + + await focusMacOSMpvProcess({ + execFile: (command, args, options, callback) => { + calls.push({ command, args, timeout: options.timeout }); + callback(null); + }, + }); + + assert.equal(calls.length, 1); + assert.equal(calls[0]?.command, '/usr/bin/osascript'); + assert.equal(calls[0]?.timeout, 2000); + assert.deepEqual(calls[0]?.args, [ + '-e', + 'tell application "System Events" to set frontmost of the first process whose name is "mpv" to true', + ]); +}); + +test('focusMacOSMpvProcess rejects when osascript fails', async () => { + await assert.rejects( + focusMacOSMpvProcess({ + execFile: (_command, _args, _options, callback) => { + callback(new Error('not allowed')); + }, + }), + /not allowed/, + ); +}); diff --git a/src/main/runtime/macos-mpv-focus.ts b/src/main/runtime/macos-mpv-focus.ts new file mode 100644 index 00000000..01d30e87 --- /dev/null +++ b/src/main/runtime/macos-mpv-focus.ts @@ -0,0 +1,35 @@ +import { execFile as nodeExecFile } from 'node:child_process'; + +const FOCUS_MPV_PROCESS_SCRIPT = + 'tell application "System Events" to set frontmost of the first process whose name is "mpv" to true'; + +type ExecFileForMacOSFocus = ( + command: string, + args: string[], + options: { timeout: number }, + callback: (error: Error | null) => void, +) => void; + +export type MacOSMpvFocusDeps = { + execFile?: ExecFileForMacOSFocus; +}; + +export async function focusMacOSMpvProcess(deps: MacOSMpvFocusDeps = {}): Promise { + const execFile: ExecFileForMacOSFocus = + deps.execFile ?? + ((command, args, options, callback) => { + nodeExecFile(command, args, options, (error) => { + callback(error); + }); + }); + + await new Promise((resolve, reject) => { + execFile('/usr/bin/osascript', ['-e', FOCUS_MPV_PROCESS_SCRIPT], { timeout: 2000 }, (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +} diff --git a/src/preload-settings.test.ts b/src/preload-settings.test.ts index e85ffcc1..0eb6e6d2 100644 --- a/src/preload-settings.test.ts +++ b/src/preload-settings.test.ts @@ -33,3 +33,16 @@ test('overlay preload buffers only latest subtitle state until renderer listener ); assert.match(source, /onSubtitle:\s*\(callback:[\s\S]+?onSubtitleSetEvent\(callback\);/); }); + +test('overlay preload exposes queued pointer recovery requests', () => { + const source = fs.readFileSync(path.join(process.cwd(), 'src', 'preload.ts'), 'utf8'); + + assert.match( + source, + /const onOverlayPointerRecoveryRequestEvent =\s*createQueuedIpcListener\(\s*IPC_CHANNELS\.event\.overlayPointerRecoveryRequest,/, + ); + assert.match( + source, + /onOverlayPointerRecoveryRequested:\s*onOverlayPointerRecoveryRequestEvent,/, + ); +}); diff --git a/src/preload.ts b/src/preload.ts index 077356e9..7cf33083 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -203,6 +203,9 @@ const onSubtitleSetEvent = createLatestValueIpcListenerWithPayload IPC_CHANNELS.event.subtitleSet, (payload) => payload as SubtitleData, ); +const onOverlayPointerRecoveryRequestEvent = createQueuedIpcListener( + IPC_CHANNELS.event.overlayPointerRecoveryRequest, +); const onSubtitleVisibilityEvent = createLatestValueIpcListenerWithPayload( IPC_CHANNELS.event.subtitleVisibility, (payload) => payload === true, @@ -225,6 +228,7 @@ const electronAPI: ElectronAPI = { onSubtitle: (callback: (data: SubtitleData) => void) => { onSubtitleSetEvent(callback); }, + onOverlayPointerRecoveryRequested: onOverlayPointerRecoveryRequestEvent, onVisibility: (callback: (visible: boolean) => void) => { onSubtitleVisibilityEvent(callback); diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index f1925067..090f1c47 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -601,6 +601,18 @@ async function init(): Promise { syncOverlayMouseIgnoreState(ctx); } + window.electronAPI.onOverlayPointerRecoveryRequested(() => { + runGuarded('overlay:pointer-recovery', () => { + if (!ctx.platform.isMacOSPlatform || !ctx.platform.shouldToggleMouseIgnore) { + return; + } + if (isAnyModalOpen()) { + return; + } + mouseHandlers.restorePointerInteractionState(); + }); + }); + await keyboardHandlers.setupMpvInputForwarding(); const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle(); diff --git a/src/shared/ipc/contracts.ts b/src/shared/ipc/contracts.ts index 56c5dedf..449eb240 100644 --- a/src/shared/ipc/contracts.ts +++ b/src/shared/ipc/contracts.ts @@ -120,6 +120,7 @@ export const IPC_CHANNELS = { }, event: { subtitleSet: 'subtitle:set', + overlayPointerRecoveryRequest: 'overlay:pointer-recovery-request', subtitleVisibility: 'mpv:subVisibility', subtitlePositionSet: 'subtitle-position:set', subtitleAssSet: 'subtitle-ass:set', diff --git a/src/types/runtime.ts b/src/types/runtime.ts index d12adbe1..bd0e82a5 100644 --- a/src/types/runtime.ts +++ b/src/types/runtime.ts @@ -404,6 +404,7 @@ export interface SessionNumericSelectionStartPayload { export interface ElectronAPI { getOverlayLayer: () => 'visible' | 'modal' | null; onSubtitle: (callback: (data: SubtitleData) => void) => void; + onOverlayPointerRecoveryRequested: (callback: () => void) => void; onVisibility: (callback: (visible: boolean) => void) => void; onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void; getOverlayVisibility: () => Promise;