From 74554a30f0f9bcc8dfb148ccabc32068cc13d224 Mon Sep 17 00:00:00 2001 From: sudacode Date: Thu, 26 Feb 2026 16:40:46 -0800 Subject: [PATCH] refactor: remove invisible subtitle overlay code --- README.md | 2 +- docs/README.md | 2 +- docs/configuration.md | 20 +- docs/installation.md | 3 - docs/mpv-plugin.md | 18 +- docs/public/config.example.jsonc | 15 +- docs/shortcuts.md | 9 +- docs/troubleshooting.md | 8 +- docs/usage.md | 14 +- plugin/subminer.conf | 6 - plugin/subminer.lua | 137 +------- src/cli/args.ts | 16 - src/cli/help.ts | 3 - src/config/config.test.ts | 49 ++- src/config/definitions.ts | 2 - src/config/definitions/defaults-core.ts | 5 - src/config/definitions/defaults-subtitle.ts | 3 +- src/config/definitions/options-subtitle.ts | 6 + src/config/definitions/template-sections.ts | 9 - src/config/resolve/subtitle-domains.ts | 20 ++ src/core/services/cli-command.test.ts | 21 -- src/core/services/cli-command.ts | 20 +- .../services/field-grouping-overlay.test.ts | 6 - src/core/services/field-grouping-overlay.ts | 4 - src/core/services/field-grouping.ts | 6 - src/core/services/ipc.test.ts | 33 +- src/core/services/ipc.ts | 60 +--- src/core/services/overlay-bridge.test.ts | 42 ++- src/core/services/overlay-bridge.ts | 23 +- .../overlay-content-measurement.test.ts | 12 +- .../services/overlay-content-measurement.ts | 3 +- src/core/services/overlay-runtime-init.ts | 19 +- .../services/overlay-shortcut-handler.test.ts | 1 - src/core/services/overlay-visibility.test.ts | 265 ++++++++++++++ src/core/services/overlay-visibility.ts | 143 ++------ src/core/services/runtime-config.test.ts | 32 +- src/core/services/shortcut.ts | 27 +- src/core/services/startup-bootstrap.test.ts | 3 - src/core/services/startup.ts | 18 +- src/core/services/subtitle-position.ts | 15 +- src/core/utils/shortcut-config.ts | 5 - src/main/cli-runtime.ts | 4 - src/main/dependencies.ts | 14 +- src/main/overlay-visibility-runtime.ts | 57 +--- .../app-lifecycle-main-activate.test.ts | 8 +- .../runtime/app-lifecycle-main-activate.ts | 6 +- .../runtime/app-runtime-main-deps.test.ts | 12 +- src/main/runtime/app-runtime-main-deps.ts | 4 +- .../runtime/cli-command-context-deps.test.ts | 2 - src/main/runtime/cli-command-context-deps.ts | 4 - .../cli-command-context-factory.test.ts | 12 +- .../cli-command-context-main-deps.test.ts | 11 +- .../runtime/cli-command-context-main-deps.ts | 4 - src/main/runtime/cli-command-context.test.ts | 2 - src/main/runtime/cli-command-context.ts | 4 - .../composers/ipc-runtime-composer.test.ts | 4 - .../composers/mpv-runtime-composer.test.ts | 2 + .../shortcuts-runtime-composer.test.ts | 1 - src/main/runtime/config-derived.ts | 5 - .../field-grouping-overlay-main-deps.test.ts | 11 +- .../field-grouping-overlay-main-deps.ts | 2 - .../global-shortcuts-main-deps.test.ts | 5 +- .../runtime/global-shortcuts-main-deps.ts | 2 - .../global-shortcuts-runtime-handlers.test.ts | 2 - src/main/runtime/global-shortcuts.test.ts | 5 +- src/main/runtime/global-shortcuts.ts | 2 - .../runtime/jellyfin-command-dispatch.test.ts | 3 - .../runtime/mpv-client-event-bindings.test.ts | 22 ++ src/main/runtime/mpv-client-event-bindings.ts | 6 +- src/main/runtime/mpv-hover-highlight.test.ts | 161 --------- src/main/runtime/mpv-hover-highlight.ts | 138 -------- .../runtime/mpv-main-event-actions.test.ts | 2 + .../overlay-bootstrap-main-deps.test.ts | 3 - .../runtime/overlay-bootstrap-main-deps.ts | 1 - ...overlay-runtime-bootstrap-handlers.test.ts | 12 +- .../runtime/overlay-runtime-bootstrap.test.ts | 8 +- src/main/runtime/overlay-runtime-bootstrap.ts | 11 +- ...lay-runtime-main-actions-main-deps.test.ts | 6 +- .../overlay-runtime-main-actions-main-deps.ts | 4 - .../overlay-runtime-main-actions.test.ts | 7 +- .../runtime/overlay-runtime-main-actions.ts | 3 - .../overlay-runtime-options-main-deps.test.ts | 13 - .../overlay-runtime-options-main-deps.ts | 21 -- .../runtime/overlay-runtime-options.test.ts | 13 - src/main/runtime/overlay-runtime-options.ts | 15 - ...erlay-visibility-actions-main-deps.test.ts | 48 --- .../overlay-visibility-actions-main-deps.ts | 29 -- .../overlay-visibility-actions.test.ts | 47 --- .../runtime/overlay-visibility-actions.ts | 46 --- ...erlay-visibility-runtime-main-deps.test.ts | 18 +- .../overlay-visibility-runtime-main-deps.ts | 9 +- .../overlay-visibility-runtime.test.ts | 37 -- .../runtime/overlay-visibility-runtime.ts | 24 -- .../overlay-window-layout-main-deps.test.ts | 19 +- .../overlay-window-layout-main-deps.ts | 14 +- .../runtime/overlay-window-layout.test.ts | 24 +- src/main/runtime/overlay-window-layout.ts | 18 +- .../runtime-bootstrap-main-deps.test.ts | 2 - .../runtime/runtime-bootstrap-main-deps.ts | 1 - src/renderer/handlers/keyboard.ts | 90 +---- src/renderer/handlers/mouse.ts | 323 +++--------------- src/renderer/modals/jimaku.ts | 1 - src/renderer/modals/kiku.ts | 1 - src/renderer/modals/runtime-options.ts | 2 - src/renderer/modals/session-help.ts | 1 - src/renderer/modals/subsync.ts | 2 - src/renderer/overlay-content-measurement.ts | 4 +- src/renderer/overlay-legacy-cleanup.test.ts | 34 ++ src/renderer/positioning/controller.ts | 33 +- .../invisible-layout-helpers.test.ts | 243 ------------- .../positioning/invisible-layout-helpers.ts | 228 ------------- .../invisible-layout-metrics.test.ts | 112 ------ .../positioning/invisible-layout-metrics.ts | 150 -------- src/renderer/positioning/invisible-layout.ts | 87 ----- src/renderer/positioning/invisible-offset.ts | 161 --------- src/renderer/positioning/position-state.ts | 21 -- src/renderer/state.ts | 43 --- src/renderer/yomitan-popup.ts | 16 + src/shared/ipc/validators.ts | 10 - 119 files changed, 691 insertions(+), 2946 deletions(-) create mode 100644 src/core/services/overlay-visibility.test.ts delete mode 100644 src/main/runtime/mpv-hover-highlight.test.ts delete mode 100644 src/main/runtime/mpv-hover-highlight.ts create mode 100644 src/renderer/overlay-legacy-cleanup.test.ts delete mode 100644 src/renderer/positioning/invisible-layout-helpers.test.ts delete mode 100644 src/renderer/positioning/invisible-layout-helpers.ts delete mode 100644 src/renderer/positioning/invisible-layout-metrics.test.ts delete mode 100644 src/renderer/positioning/invisible-layout-metrics.ts delete mode 100644 src/renderer/positioning/invisible-layout.ts delete mode 100644 src/renderer/positioning/invisible-offset.ts create mode 100644 src/renderer/yomitan-popup.ts diff --git a/README.md b/README.md index 82ba7af..8c122a3 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ subminer app --start --yomitan ```bash subminer app --start --background -subminer video.mkv # toggle invisible overlay with y-i and visible overlay with y-t +subminer video.mkv # y-t toggles overlay visibility ``` ## Requirements diff --git a/docs/README.md b/docs/README.md index 8b8809b..1cd8d65 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ make docs-preview # Preview built site at http://localhost:4173 - [Installation](/installation) — Requirements, Linux/macOS/Windows install, mpv plugin setup - [Usage](/usage) — `subminer` wrapper + subcommands (`jellyfin`, `yt`, `doctor`, `config`, `mpv`, `texthooker`, `app`), mpv plugin, keybindings -- [Mining Workflow](/mining-workflow) — End-to-end sentence mining guide, overlay layers, card creation +- [Mining Workflow](/mining-workflow) — End-to-end sentence mining guide, single overlay + modals, card creation ### Reference diff --git a/docs/configuration.md b/docs/configuration.md index b24566a..d747bb9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -74,7 +74,7 @@ The configuration file includes several main sections: - [**Auto-Start Overlay**](#auto-start-overlay) - Automatically show overlay on MPV connection - [**Visible Overlay Subtitle Binding**](#visible-overlay-subtitle-binding) - Link visible overlay toggles to MPV subtitle visibility - [**Auto Subtitle Sync**](#auto-subtitle-sync) - Sync current subtitle with `alass`/`ffsubsync` -- [**Invisible Overlay**](#invisible-overlay) - Startup visibility behavior for the invisible mining layer +- [**Subtitle Position Edit**](#subtitle-position-edit) - Fine-tune subtitle alignment in overlay - [**Jimaku**](#jimaku) - Jimaku API configuration and defaults - [**AniList**](#anilist) - Optional post-watch progress updates - [**Jellyfin**](#jellyfin) - Optional Jellyfin auth, library listing, and playback launch @@ -338,7 +338,7 @@ Control whether the overlay automatically becomes visible when it connects to mp | -------------------- | --------------- | ------------------------------------------------------ | | `auto_start_overlay` | `true`, `false` | Auto-show overlay on mpv connection (default: `false`) | -The mpv plugin controls startup per layer via `auto_start_visible_overlay` and `auto_start_invisible_overlay` in `subminer.conf` (`platform-default` for invisible means hidden on Linux, visible on macOS/Windows). +The mpv plugin controls startup overlay visibility via `auto_start_visible_overlay` in `subminer.conf`. ### Visible Overlay Subtitle Binding @@ -379,20 +379,12 @@ Sync the active subtitle track using `alass` (preferred) or `ffsubsync`: Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`. Customize it there, or set it to `null` to disable. -### Invisible Overlay +### Subtitle Position Edit -SubMiner includes a second subtitle mining layer that can be visually invisible while still interactive for Yomitan lookups. - -- `invisibleOverlay.startupVisibility` values: - -1. `"platform-default"`: hidden on Wayland, visible on Windows/macOS/other sessions. -2. `"visible"`: always shown on startup. -3. `"hidden"`: always hidden on startup. - -Invisible subtitle positioning can be adjusted directly in the invisible layer: +Subtitle positioning can be adjusted directly in the overlay: - `Ctrl/Cmd+Shift+P` toggles position edit mode. -- Use arrow keys to move the invisible subtitle text. +- Use arrow keys to move subtitle text. - Press `Enter` or `Ctrl/Cmd+S` to save, or `Esc` to cancel. - This edit-mode shortcut is fixed (not currently configurable in `shortcuts`/`keybindings`). @@ -670,7 +662,6 @@ See `config.example.jsonc` for detailed configuration options. { "shortcuts": { "toggleVisibleOverlayGlobal": "Alt+Shift+O", - "toggleInvisibleOverlayGlobal": "Alt+Shift+I", "copySubtitle": "CommandOrControl+C", "copySubtitleMultiple": "CommandOrControl+Shift+C", "updateLastCardFromClipboard": "CommandOrControl+V", @@ -689,7 +680,6 @@ See `config.example.jsonc` for detailed configuration options. | Option | Values | Description | | ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) | -| `toggleInvisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling invisible interactive overlay (default: `"Alt+Shift+I"`) | | `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) | | `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+C"`) | | `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) | diff --git a/docs/installation.md b/docs/installation.md index 18728c4..75be63e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -181,9 +181,6 @@ All keybindings use a `y` chord prefix — press `y`, then the second key: | `y-s` | Start overlay | | `y-S` | Stop overlay | | `y-t` | Toggle visible overlay | -| `y-i` | Toggle invisible overlay | -| `y-I` | Show invisible overlay | -| `y-u` | Hide invisible overlay | | `y-o` | Open Yomitan settings | | `y-r` | Restart overlay | | `y-c` | Check overlay status | diff --git a/docs/mpv-plugin.md b/docs/mpv-plugin.md index 72be4cb..9a489e0 100644 --- a/docs/mpv-plugin.md +++ b/docs/mpv-plugin.md @@ -33,9 +33,6 @@ All keybindings use a `y` chord prefix — press `y`, then the second key: | `y-s` | Start overlay | | `y-S` | Stop overlay | | `y-t` | Toggle visible overlay | -| `y-i` | Toggle invisible overlay | -| `y-I` | Show invisible overlay | -| `y-u` | Hide invisible overlay | | `y-o` | Open settings window | | `y-r` | Restart overlay | | `y-c` | Check status | @@ -50,10 +47,9 @@ SubMiner: 1. Start overlay 2. Stop overlay 3. Toggle overlay -4. Toggle invisible overlay -5. Open options -6. Restart overlay -7. Check status +4. Open options +5. Restart overlay +6. Check status ``` Select an item by pressing its number. @@ -84,10 +80,6 @@ auto_start=no # Show the visible overlay on auto-start. auto_start_visible_overlay=no -# Invisible overlay startup: platform-default, visible, hidden. -# platform-default = hidden on Linux, visible on macOS/Windows. -auto_start_invisible_overlay=platform-default - # Show OSD messages for overlay status changes. osd_messages=yes @@ -129,7 +121,6 @@ aniskip_button_duration=3 | `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend | | `auto_start` | `no` | `yes` / `no` | Auto-start overlay on file load | | `auto_start_visible_overlay` | `no` | `yes` / `no` | Show visible layer on auto-start | -| `auto_start_invisible_overlay` | `platform-default` | `platform-default`, `visible`, `hidden` | Invisible layer on auto-start | | `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages | | `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity | | `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection | @@ -182,9 +173,6 @@ The plugin can be controlled from other mpv scripts or the mpv command line usin script-message subminer-start script-message subminer-stop script-message subminer-toggle -script-message subminer-toggle-invisible -script-message subminer-show-invisible -script-message subminer-hide-invisible script-message subminer-menu script-message subminer-options script-message subminer-restart diff --git a/docs/public/config.example.jsonc b/docs/public/config.example.jsonc index f7f4926..4c0103c 100644 --- a/docs/public/config.example.jsonc +++ b/docs/public/config.example.jsonc @@ -53,7 +53,6 @@ // ========================================== "shortcuts": { "toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting. - "toggleInvisibleOverlayGlobal": "Alt+Shift+I", // Toggle invisible overlay global setting. "copySubtitle": "CommandOrControl+C", // Copy subtitle setting. "copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting. "updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting. @@ -68,16 +67,6 @@ "openJimaku": "Ctrl+Shift+J" // Open jimaku setting. }, // Overlay keyboard shortcuts. Set a shortcut to null to disable. - // ========================================== - // Invisible Overlay - // Startup behavior for the invisible interactive subtitle mining layer. - // Invisible subtitle position edit mode: Ctrl/Cmd+Shift+P to toggle, arrow keys to move, Enter or Ctrl/Cmd+S to save, Esc to cancel. - // This edit-mode shortcut is fixed and is not currently configurable. - // ========================================== - "invisibleOverlay": { - "startupVisibility": "platform-default" // Startup visibility setting. - }, // Startup behavior for the invisible interactive subtitle mining layer. - // ========================================== // Keybindings (MPV Commands) // Extra keybindings that are merged with built-in defaults. @@ -123,9 +112,11 @@ // Hot-reload: subtitle style changes apply live without restarting SubMiner. // ========================================== "subtitleStyle": { + // Additional CSS declarations are also allowed and applied directly to subtitle roots/containers (for example: lineHeight, textShadow, -webkit-text-stroke, backdropFilter). "enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false "preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false - "hoverTokenColor": "#c6a0f6", // Hex color used for hovered subtitle token highlight in mpv. + "hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in the overlay. + "hoverTokenBackgroundColor": "#363a4fd6", // CSS color used for hovered subtitle token background highlight in the overlay. "fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif", // Font family setting. "fontSize": 35, // Font size setting. "fontColor": "#cad3f5", // Font color setting. diff --git a/docs/shortcuts.md b/docs/shortcuts.md index 4ce1285..934eec7 100644 --- a/docs/shortcuts.md +++ b/docs/shortcuts.md @@ -9,7 +9,6 @@ These work system-wide regardless of which window has focus. | Shortcut | Action | Configurable | | ------------- | ------------------------ | ---------------------------------------- | | `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` | -| `Alt+Shift+I` | Toggle invisible overlay | `shortcuts.toggleInvisibleOverlayGlobal` | | `Alt+Shift+Y` | Open Yomitan settings | Fixed (not configurable) | ::: tip @@ -64,9 +63,9 @@ These keybindings can be overridden or disabled via the `keybindings` config arr | `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` | | `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` | -## Invisible Subtitle Position Edit Mode +## Subtitle Position Edit Mode -Enter edit mode to fine-tune invisible overlay alignment with mpv's native subtitles. +Enter edit mode to fine-tune subtitle alignment. | Shortcut | Action | | --------------------- | -------------------------------- | @@ -86,9 +85,6 @@ When the mpv plugin is installed, all commands use a `y` chord prefix — press | `y-s` | Start overlay | | `y-S` | Stop overlay | | `y-t` | Toggle visible overlay | -| `y-i` | Toggle invisible overlay | -| `y-I` | Show invisible overlay | -| `y-u` | Hide invisible overlay | | `y-o` | Open Yomitan settings | | `y-r` | Restart overlay | | `y-c` | Check overlay status | @@ -112,7 +108,6 @@ All `shortcuts.*` keys accept [Electron accelerator strings](https://www.electro "mineSentence": "CommandOrControl+S", "copySubtitle": "CommandOrControl+C", "toggleVisibleOverlayGlobal": "Alt+Shift+O", - "toggleInvisibleOverlayGlobal": "Alt+Shift+I", "openJimaku": null, // disabled }, } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 94c049e..ebf70f3 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -153,7 +153,7 @@ SubMiner positions the overlay by tracking the mpv window. If tracking fails: - Sway: Ensure `swaymsg` is available. - X11: Ensure `xdotool` and `xwininfo` are installed. -If the overlay position is slightly off, use invisible subtitle position edit mode (`Ctrl/Cmd+Shift+P`) to fine-tune the offset with arrow keys, then save with `Enter` or `Ctrl+S`. +If the overlay position is slightly off, use subtitle position edit mode (`Ctrl/Cmd+Shift+P`) to fine-tune the offset with arrow keys, then save with `Enter` or `Ctrl+S`. ## Yomitan @@ -217,10 +217,10 @@ Media generation has a 30-second timeout (60 seconds for animated AVIF). If your **"Failed to register global shortcut"** -Global shortcuts (`Alt+Shift+O`, `Alt+Shift+I`, `Alt+Shift+Y`) may conflict with other applications or desktop environment keybindings. +Global shortcuts (`Alt+Shift+O`, `Alt+Shift+Y`) may conflict with other applications or desktop environment keybindings. - Check your DE/WM keybinding settings for conflicts. -- Change the shortcuts in your config under `shortcuts.toggleVisibleOverlayGlobal`, `shortcuts.toggleInvisibleOverlayGlobal`. +- Change the shortcut in your config under `shortcuts.toggleVisibleOverlayGlobal`. - On Wayland, global shortcut registration has limitations depending on the compositor. **Overlay keybindings not working** @@ -273,5 +273,5 @@ The Jimaku API has rate limits. If you see 429 errors, wait for the retry durati ### macOS - **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility. -- **Font rendering**: macOS uses a 0.87x font compensation factor for subtitle alignment between mpv and the overlay. If text alignment looks off, adjust the invisible subtitle offset. +- **Font rendering**: macOS uses a 0.87x font compensation factor for subtitle alignment between mpv and the overlay. If text alignment looks off, adjust subtitle offset in position edit mode. - **Gatekeeper**: If macOS blocks SubMiner, right-click the app and select "Open" to bypass the warning, or remove the quarantine attribute: `xattr -d com.apple.quarantine /path/to/SubMiner.app` diff --git a/docs/usage.md b/docs/usage.md index 4e1273c..e7a094a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -5,7 +5,7 @@ There are two ways to use SubMiner — the `subminer` wrapper script or the mpv | Approach | Best For | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, and manages app commands. Overlay start is explicit (`--start`, `-S`, or `y-s`). | -| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. | +| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control overlay visibility. Requires `--input-ipc-server=/tmp/subminer-socket`. | You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow. @@ -68,11 +68,8 @@ SubMiner.AppImage --start --texthooker # Start overlay with texthooker SubMiner.AppImage --texthooker # Launch texthooker only (no overlay window) SubMiner.AppImage --stop # Stop overlay SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility -SubMiner.AppImage --start --toggle-invisible-overlay # Start MPV IPC + toggle invisible layer SubMiner.AppImage --show-visible-overlay # Force show visible overlay SubMiner.AppImage --hide-visible-overlay # Force hide visible overlay -SubMiner.AppImage --show-invisible-overlay # Force show invisible overlay -SubMiner.AppImage --hide-invisible-overlay # Force hide invisible overlay SubMiner.AppImage --start --dev # Enable app/dev mode only SubMiner.AppImage --start --debug # Alias for --dev SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode @@ -173,7 +170,6 @@ Notes: | Keybind | Action | | ------------- | ------------------------ | | `Alt+Shift+O` | Toggle visible overlay | -| `Alt+Shift+I` | Toggle invisible overlay | | `Alt+Shift+Y` | Open Yomitan settings | `Alt+Shift+Y` is a fixed global shortcut; it is not part of `shortcuts` config. @@ -195,10 +191,10 @@ Notes: | `Ctrl+W` | Quit mpv | | `Right-click` | Toggle MPV pause (outside subtitle area) | | `Right-click + drag` | Move subtitle position (on subtitle) | -| `Ctrl/Cmd+Shift+P` | Toggle invisible subtitle position edit mode | -| `Arrow keys` | Move invisible subtitles while edit mode is active | -| `Enter` / `Ctrl+S` | Save invisible subtitle position in edit mode | -| `Esc` | Cancel invisible subtitle position edit mode | +| `Ctrl/Cmd+Shift+P` | Toggle subtitle position edit mode | +| `Arrow keys` | Move subtitles while edit mode is active | +| `Enter` / `Ctrl+S` | Save subtitle position in edit mode | +| `Esc` | Cancel subtitle position edit mode | | `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist | These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization. diff --git a/plugin/subminer.conf b/plugin/subminer.conf index ec489d8..9b975fa 100644 --- a/plugin/subminer.conf +++ b/plugin/subminer.conf @@ -26,11 +26,6 @@ auto_start=no # Automatically show visible overlay when overlay starts auto_start_visible_overlay=no -# Automatically show invisible overlay when overlay starts -# Values: platform-default, visible, hidden -# platform-default => hidden on Linux, visible on macOS/Windows -auto_start_invisible_overlay=platform-default - # Legacy alias (maps to auto_start_visible_overlay) # auto_start_overlay=no @@ -70,4 +65,3 @@ aniskip_button_duration=3 # MPV keybindings provided by plugin/subminer.lua: # y-s start, y-S stop, y-t toggle visible overlay -# y-i toggle invisible overlay, y-I show invisible overlay, y-u hide invisible overlay diff --git a/plugin/subminer.lua b/plugin/subminer.lua index d1a8649..d5a9a16 100644 --- a/plugin/subminer.lua +++ b/plugin/subminer.lua @@ -24,10 +24,6 @@ local function default_socket_path() return "/tmp/subminer-socket" end -local function is_linux() - return not is_windows() and not is_macos() -end - local function is_subminer_process_running() local command = is_windows() and { "tasklist", "/FO", "CSV", "/NH" } or { "ps", "-A", "-o", "args=" } local result = mp.command_native({ @@ -140,7 +136,6 @@ local opts = { auto_start = true, auto_start_overlay = false, -- legacy alias, maps to auto_start_visible_overlay auto_start_visible_overlay = false, - auto_start_invisible_overlay = "platform-default", -- platform-default | visible | hidden osd_messages = true, log_level = "info", aniskip_enabled = true, @@ -163,7 +158,6 @@ local state = { binary_available = false, binary_path = nil, detected_backend = nil, - invisible_overlay_visible = false, hover_highlight = { revision = -1, payload = nil, @@ -796,6 +790,15 @@ local function fix_ass_color(input, fallback) return b .. g .. r end +local function sanitize_hover_ass_color(input, fallback_rgb) + local fallback = fix_ass_color(fallback_rgb or DEFAULT_HOVER_COLOR, DEFAULT_HOVER_COLOR) + local converted = fix_ass_color(input, fallback) + if converted == "000000" then + return fallback + end + return converted +end + local function escape_ass_text(text) return (text or "") :gsub("\\", "\\\\") @@ -858,7 +861,7 @@ local function resolve_metrics() border = sub_border_size * window_scale, shadow = sub_shadow_offset * window_scale, base_color = fix_ass_color(mp.get_property("sub-color"), DEFAULT_HOVER_BASE_COLOR), - hover_color = fix_ass_color(mp.get_property("sub-color"), DEFAULT_HOVER_COLOR), + hover_color = sanitize_hover_ass_color(nil, DEFAULT_HOVER_COLOR), } end @@ -1068,7 +1071,7 @@ local function build_hover_subtitle_content(payload) end local metrics = resolve_metrics() - local hover_color = fix_ass_color(payload.colors and payload.colors.hover or nil, metrics.hover_color) + local hover_color = sanitize_hover_ass_color(payload.colors and payload.colors.hover or nil, DEFAULT_HOVER_COLOR) local base_color = fix_ass_color(payload.colors and payload.colors.base or nil, metrics.base_color) return inject_hover_color_to_ass(source_ass, plain_map, hover_start, hover_end, hover_color, base_color) end @@ -1443,39 +1446,13 @@ local function resolve_visible_overlay_startup() return visible end -local function resolve_invisible_overlay_startup() - local raw = opts.auto_start_invisible_overlay - if type(raw) == "boolean" then - return raw - end - - local mode = type(raw) == "string" and raw:lower() or "platform-default" - if mode == "visible" or mode == "show" or mode == "yes" or mode == "true" or mode == "on" then - return true - end - if mode == "hidden" or mode == "hide" or mode == "no" or mode == "false" or mode == "off" then - return false - end - - -- platform-default - return not is_linux() -end - local function apply_startup_overlay_preferences() local should_show_visible = resolve_visible_overlay_startup() - local should_show_invisible = resolve_invisible_overlay_startup() local visible_action = should_show_visible and "show-visible-overlay" or "hide-visible-overlay" if not run_control_command(visible_action) then subminer_log("warn", "process", "Failed to apply visible startup action: " .. visible_action) end - - local invisible_action = should_show_invisible and "show-invisible-overlay" or "hide-invisible-overlay" - if not run_control_command(invisible_action) then - subminer_log("warn", "process", "Failed to apply invisible startup action: " .. invisible_action) - end - - state.invisible_overlay_visible = should_show_invisible end local function build_texthooker_args() @@ -1646,90 +1623,6 @@ local function toggle_overlay() end end -local function toggle_invisible_overlay() - if not ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - - local args = build_command_args("toggle-invisible-overlay") - subminer_log("info", "process", "Toggling invisible overlay: " .. table.concat(args, " ")) - - local result = mp.command_native({ - name = "subprocess", - args = args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }) - - if result and result.status ~= 0 then - subminer_log("warn", "process", "Invisible toggle command failed") - show_osd("Invisible toggle failed") - return - end - - state.invisible_overlay_visible = not state.invisible_overlay_visible - show_osd("Invisible overlay: " .. (state.invisible_overlay_visible and "visible" or "hidden")) -end - -local function show_invisible_overlay() - if not ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - - local args = build_command_args("show-invisible-overlay") - subminer_log("info", "process", "Showing invisible overlay: " .. table.concat(args, " ")) - - local result = mp.command_native({ - name = "subprocess", - args = args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }) - - if result and result.status ~= 0 then - subminer_log("warn", "process", "Show invisible command failed") - show_osd("Show invisible failed") - return - end - - state.invisible_overlay_visible = true - show_osd("Invisible overlay: visible") -end - -local function hide_invisible_overlay() - if not ensure_binary_available() then - subminer_log("error", "binary", "SubMiner binary not found") - show_osd("Error: binary not found") - return - end - - local args = build_command_args("hide-invisible-overlay") - subminer_log("info", "process", "Hiding invisible overlay: " .. table.concat(args, " ")) - - local result = mp.command_native({ - name = "subprocess", - args = args, - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }) - - if result and result.status ~= 0 then - subminer_log("warn", "process", "Hide invisible command failed") - show_osd("Hide invisible failed") - return - end - - state.invisible_overlay_visible = false - show_osd("Invisible overlay: hidden") -end - local function open_options() if not state.binary_available then subminer_log("error", "binary", "SubMiner binary not found") @@ -1768,7 +1661,6 @@ local function show_menu() "Start overlay", "Stop overlay", "Toggle overlay", - "Toggle invisible overlay", "Open options", "Restart overlay", "Check status", @@ -1778,7 +1670,6 @@ local function show_menu() start_overlay, stop_overlay, toggle_overlay, - toggle_invisible_overlay, open_options, restart_overlay, check_status, @@ -1895,9 +1786,6 @@ local function register_keybindings() mp.add_key_binding("y-s", "subminer-start", start_overlay) mp.add_key_binding("y-S", "subminer-stop", stop_overlay) mp.add_key_binding("y-t", "subminer-toggle", toggle_overlay) - mp.add_key_binding("y-i", "subminer-toggle-invisible", toggle_invisible_overlay) - mp.add_key_binding("y-I", "subminer-show-invisible", show_invisible_overlay) - mp.add_key_binding("y-u", "subminer-hide-invisible", hide_invisible_overlay) mp.add_key_binding("y-y", "subminer-menu", show_menu) mp.add_key_binding("y-o", "subminer-options", open_options) mp.add_key_binding("y-r", "subminer-restart", restart_overlay) @@ -1914,9 +1802,6 @@ local function register_script_messages() mp.register_script_message("subminer-start", start_overlay_from_script_message) mp.register_script_message("subminer-stop", stop_overlay) mp.register_script_message("subminer-toggle", toggle_overlay) - mp.register_script_message("subminer-toggle-invisible", toggle_invisible_overlay) - mp.register_script_message("subminer-show-invisible", show_invisible_overlay) - mp.register_script_message("subminer-hide-invisible", hide_invisible_overlay) mp.register_script_message("subminer-menu", show_menu) mp.register_script_message("subminer-options", open_options) mp.register_script_message("subminer-restart", restart_overlay) diff --git a/src/cli/args.ts b/src/cli/args.ts index a031d54..39db2d6 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -4,14 +4,11 @@ export interface CliArgs { stop: boolean; toggle: boolean; toggleVisibleOverlay: boolean; - toggleInvisibleOverlay: boolean; settings: boolean; show: boolean; hide: boolean; showVisibleOverlay: boolean; hideVisibleOverlay: boolean; - showInvisibleOverlay: boolean; - hideInvisibleOverlay: boolean; copySubtitle: boolean; copySubtitleMultiple: boolean; mineSentence: boolean; @@ -67,14 +64,11 @@ export function parseArgs(argv: string[]): CliArgs { stop: false, toggle: false, toggleVisibleOverlay: false, - toggleInvisibleOverlay: false, settings: false, show: false, hide: false, showVisibleOverlay: false, hideVisibleOverlay: false, - showInvisibleOverlay: false, - hideInvisibleOverlay: false, copySubtitle: false, copySubtitleMultiple: false, mineSentence: false, @@ -122,14 +116,11 @@ export function parseArgs(argv: string[]): CliArgs { else if (arg === '--stop') args.stop = true; else if (arg === '--toggle') args.toggle = true; else if (arg === '--toggle-visible-overlay') args.toggleVisibleOverlay = true; - else if (arg === '--toggle-invisible-overlay') args.toggleInvisibleOverlay = true; else if (arg === '--settings' || arg === '--yomitan') args.settings = true; else if (arg === '--show') args.show = true; else if (arg === '--hide') args.hide = true; else if (arg === '--show-visible-overlay') args.showVisibleOverlay = true; else if (arg === '--hide-visible-overlay') args.hideVisibleOverlay = true; - else if (arg === '--show-invisible-overlay') args.showInvisibleOverlay = true; - else if (arg === '--hide-invisible-overlay') args.hideInvisibleOverlay = true; else if (arg === '--copy-subtitle') args.copySubtitle = true; else if (arg === '--copy-subtitle-multiple') args.copySubtitleMultiple = true; else if (arg === '--mine-sentence') args.mineSentence = true; @@ -263,14 +254,11 @@ export function hasExplicitCommand(args: CliArgs): boolean { args.stop || args.toggle || args.toggleVisibleOverlay || - args.toggleInvisibleOverlay || args.settings || args.show || args.hide || args.showVisibleOverlay || args.hideVisibleOverlay || - args.showInvisibleOverlay || - args.hideInvisibleOverlay || args.copySubtitle || args.copySubtitleMultiple || args.mineSentence || @@ -307,7 +295,6 @@ export function shouldStartApp(args: CliArgs): boolean { args.start || args.toggle || args.toggleVisibleOverlay || - args.toggleInvisibleOverlay || args.copySubtitle || args.copySubtitleMultiple || args.mineSentence || @@ -331,13 +318,10 @@ export function commandNeedsOverlayRuntime(args: CliArgs): boolean { return ( args.toggle || args.toggleVisibleOverlay || - args.toggleInvisibleOverlay || args.show || args.hide || args.showVisibleOverlay || args.hideVisibleOverlay || - args.showInvisibleOverlay || - args.hideInvisibleOverlay || args.copySubtitle || args.copySubtitleMultiple || args.mineSentence || diff --git a/src/cli/help.ts b/src/cli/help.ts index ecdb0e1..c35658b 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -17,11 +17,8 @@ ${B}Session${R} ${B}Overlay${R} --toggle-visible-overlay Toggle subtitle overlay - --toggle-invisible-overlay Toggle interactive overlay ${D}(Yomitan lookup)${R} --show-visible-overlay Show subtitle overlay --hide-visible-overlay Hide subtitle overlay - --show-invisible-overlay Show interactive overlay - --hide-invisible-overlay Hide interactive overlay --settings Open Yomitan settings window --auto-start-overlay Auto-hide mpv subs, show overlay on connect diff --git a/src/config/config.test.ts b/src/config/config.test.ts index ed35864..0337efa 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -27,7 +27,8 @@ test('loads defaults when config is missing', () => { assert.equal(config.discordPresence.updateIntervalMs, 3_000); assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)'); assert.equal(config.subtitleStyle.preserveLineBreaks, false); - assert.equal(config.subtitleStyle.hoverTokenColor, '#c6a0f6'); + assert.equal(config.subtitleStyle.hoverTokenColor, '#f4dbd6'); + assert.equal(config.subtitleStyle.hoverTokenBackgroundColor, '#363a4fd6'); assert.equal(config.immersionTracking.enabled, true); assert.equal(config.immersionTracking.dbPath, ''); assert.equal(config.immersionTracking.batchSize, 25); @@ -136,6 +137,44 @@ test('parses subtitleStyle.hoverTokenColor and warns on invalid values', () => { ); }); +test('parses subtitleStyle.hoverTokenBackgroundColor and warns on invalid values', () => { + const validDir = makeTempDir(); + fs.writeFileSync( + path.join(validDir, 'config.jsonc'), + `{ + "subtitleStyle": { + "hoverTokenBackgroundColor": "#363a4fd6" + } + }`, + 'utf-8', + ); + + const validService = new ConfigService(validDir); + assert.equal(validService.getConfig().subtitleStyle.hoverTokenBackgroundColor, '#363a4fd6'); + + const invalidDir = makeTempDir(); + fs.writeFileSync( + path.join(invalidDir, 'config.jsonc'), + `{ + "subtitleStyle": { + "hoverTokenBackgroundColor": true + } + }`, + 'utf-8', + ); + + const invalidService = new ConfigService(invalidDir); + assert.equal( + invalidService.getConfig().subtitleStyle.hoverTokenBackgroundColor, + DEFAULT_CONFIG.subtitleStyle.hoverTokenBackgroundColor, + ); + assert.ok( + invalidService + .getWarnings() + .some((warning) => warning.path === 'subtitleStyle.hoverTokenBackgroundColor'), + ); +}); + test('parses anilist.enabled and warns for invalid value', () => { const dir = makeTempDir(); fs.writeFileSync( @@ -597,19 +636,15 @@ test('warns and ignores unknown top-level config keys', () => { assert.ok(warnings.some((warning) => warning.path === 'unknownFeatureFlag')); }); -test('parses invisible overlay config and new global shortcuts', () => { +test('parses global shortcuts and startup visibility flags', () => { const dir = makeTempDir(); fs.writeFileSync( path.join(dir, 'config.jsonc'), `{ "shortcuts": { "toggleVisibleOverlayGlobal": "Alt+Shift+U", - "toggleInvisibleOverlayGlobal": "Alt+Shift+I", "openJimaku": "Ctrl+Alt+J" }, - "invisibleOverlay": { - "startupVisibility": "hidden" - }, "bind_visible_overlay_to_mpv_sub_visibility": false, "youtubeSubgen": { "primarySubLanguages": ["ja", "jpn", "jp"] @@ -621,9 +656,7 @@ test('parses invisible overlay config and new global shortcuts', () => { const service = new ConfigService(dir); const config = service.getConfig(); assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, 'Alt+Shift+U'); - assert.equal(config.shortcuts.toggleInvisibleOverlayGlobal, 'Alt+Shift+I'); assert.equal(config.shortcuts.openJimaku, 'Ctrl+Alt+J'); - assert.equal(config.invisibleOverlay.startupVisibility, 'hidden'); assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false); assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'jpn', 'jp']); }); diff --git a/src/config/definitions.ts b/src/config/definitions.ts index f471b4e..3827dcb 100644 --- a/src/config/definitions.ts +++ b/src/config/definitions.ts @@ -29,7 +29,6 @@ const { subsync, auto_start_overlay, bind_visible_overlay_to_mpv_sub_visibility, - invisibleOverlay, } = CORE_DEFAULT_CONFIG; const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } = INTEGRATIONS_DEFAULT_CONFIG; @@ -54,7 +53,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = { jellyfin, discordPresence, youtubeSubgen, - invisibleOverlay, immersionTracking, }; diff --git a/src/config/definitions/defaults-core.ts b/src/config/definitions/defaults-core.ts index 9097066..3703443 100644 --- a/src/config/definitions/defaults-core.ts +++ b/src/config/definitions/defaults-core.ts @@ -12,7 +12,6 @@ export const CORE_DEFAULT_CONFIG: Pick< | 'subsync' | 'auto_start_overlay' | 'bind_visible_overlay_to_mpv_sub_visibility' - | 'invisibleOverlay' > = { subtitlePosition: { yPercent: 10 }, keybindings: [], @@ -28,7 +27,6 @@ export const CORE_DEFAULT_CONFIG: Pick< }, shortcuts: { toggleVisibleOverlayGlobal: 'Alt+Shift+O', - toggleInvisibleOverlayGlobal: 'Alt+Shift+I', copySubtitle: 'CommandOrControl+C', copySubtitleMultiple: 'CommandOrControl+Shift+C', updateLastCardFromClipboard: 'CommandOrControl+V', @@ -55,7 +53,4 @@ export const CORE_DEFAULT_CONFIG: Pick< }, auto_start_overlay: false, bind_visible_overlay_to_mpv_sub_visibility: true, - invisibleOverlay: { - startupVisibility: 'platform-default', - }, }; diff --git a/src/config/definitions/defaults-subtitle.ts b/src/config/definitions/defaults-subtitle.ts index 00706e9..315f114 100644 --- a/src/config/definitions/defaults-subtitle.ts +++ b/src/config/definitions/defaults-subtitle.ts @@ -4,7 +4,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick = { subtitleStyle: { enableJlpt: false, preserveLineBreaks: false, - hoverTokenColor: '#c6a0f6', + hoverTokenColor: '#f4dbd6', + hoverTokenBackgroundColor: '#363a4fd6', fontFamily: 'M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif', fontSize: 35, diff --git a/src/config/definitions/options-subtitle.ts b/src/config/definitions/options-subtitle.ts index e7d5bf7..04a3362 100644 --- a/src/config/definitions/options-subtitle.ts +++ b/src/config/definitions/options-subtitle.ts @@ -27,6 +27,12 @@ export function buildSubtitleConfigOptionRegistry( defaultValue: defaultConfig.subtitleStyle.hoverTokenColor, description: 'Hex color used for hovered subtitle token highlight in mpv.', }, + { + path: 'subtitleStyle.hoverTokenBackgroundColor', + kind: 'string', + defaultValue: defaultConfig.subtitleStyle.hoverTokenBackgroundColor, + description: 'CSS color used for hovered subtitle token background highlight in mpv.', + }, { path: 'subtitleStyle.frequencyDictionary.enabled', kind: 'boolean', diff --git a/src/config/definitions/template-sections.ts b/src/config/definitions/template-sections.ts index a4a5a4f..20d387f 100644 --- a/src/config/definitions/template-sections.ts +++ b/src/config/definitions/template-sections.ts @@ -40,15 +40,6 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [ notes: ['Hot-reload: shortcut changes apply live and update the session help modal on reopen.'], key: 'shortcuts', }, - { - title: 'Invisible Overlay', - description: ['Startup behavior for the invisible interactive subtitle mining layer.'], - notes: [ - 'Invisible subtitle position edit mode: Ctrl/Cmd+Shift+P to toggle, arrow keys to move, Enter or Ctrl/Cmd+S to save, Esc to cancel.', - 'This edit-mode shortcut is fixed and is not currently configurable.', - ], - key: 'invisibleOverlay', - }, { title: 'Keybindings (MPV Commands)', description: [ diff --git a/src/config/resolve/subtitle-domains.ts b/src/config/resolve/subtitle-domains.ts index 2b0144a..0307574 100644 --- a/src/config/resolve/subtitle-domains.ts +++ b/src/config/resolve/subtitle-domains.ts @@ -100,6 +100,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt; const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks; const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor; + const fallbackSubtitleStyleHoverTokenBackgroundColor = + resolved.subtitleStyle.hoverTokenBackgroundColor; resolved.subtitleStyle = { ...resolved.subtitleStyle, ...(src.subtitleStyle as ResolvedConfig['subtitleStyle']), @@ -154,6 +156,24 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { ); } + const hoverTokenBackgroundColor = asString( + (src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor, + ); + if (hoverTokenBackgroundColor !== undefined) { + resolved.subtitleStyle.hoverTokenBackgroundColor = hoverTokenBackgroundColor; + } else if ( + (src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor !== + undefined + ) { + resolved.subtitleStyle.hoverTokenBackgroundColor = fallbackSubtitleStyleHoverTokenBackgroundColor; + warn( + 'subtitleStyle.hoverTokenBackgroundColor', + (src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor, + resolved.subtitleStyle.hoverTokenBackgroundColor, + 'Expected string.', + ); + } + const frequencyDictionary = isObject( (src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary, ) diff --git a/src/core/services/cli-command.test.ts b/src/core/services/cli-command.test.ts index cd2ae62..76563a5 100644 --- a/src/core/services/cli-command.test.ts +++ b/src/core/services/cli-command.test.ts @@ -10,14 +10,11 @@ function makeArgs(overrides: Partial = {}): CliArgs { stop: false, toggle: false, toggleVisibleOverlay: false, - toggleInvisibleOverlay: false, settings: false, show: false, hide: false, showVisibleOverlay: false, hideVisibleOverlay: false, - showInvisibleOverlay: false, - hideInvisibleOverlay: false, copySubtitle: false, copySubtitleMultiple: false, mineSentence: false, @@ -94,18 +91,12 @@ function createDeps(overrides: Partial = {}) { toggleVisibleOverlay: () => { calls.push('toggleVisibleOverlay'); }, - toggleInvisibleOverlay: () => { - calls.push('toggleInvisibleOverlay'); - }, openYomitanSettingsDelayed: (delayMs) => { calls.push(`openYomitanSettingsDelayed:${delayMs}`); }, setVisibleOverlayVisible: (visible) => { calls.push(`setVisibleOverlayVisible:${visible}`); }, - setInvisibleOverlayVisible: (visible) => { - calls.push(`setInvisibleOverlayVisible:${visible}`); - }, copyCurrentSubtitle: () => { calls.push('copyCurrentSubtitle'); }, @@ -339,10 +330,6 @@ test('handleCliCommand handles visibility and utility command dispatches', () => args: Partial; expected: string; }> = [ - { - args: { toggleInvisibleOverlay: true }, - expected: 'toggleInvisibleOverlay', - }, { args: { settings: true }, expected: 'openYomitanSettingsDelayed:1000' }, { args: { showVisibleOverlay: true }, @@ -352,14 +339,6 @@ test('handleCliCommand handles visibility and utility command dispatches', () => args: { hideVisibleOverlay: true }, expected: 'setVisibleOverlayVisible:false', }, - { - args: { showInvisibleOverlay: true }, - expected: 'setInvisibleOverlayVisible:true', - }, - { - args: { hideInvisibleOverlay: true }, - expected: 'setInvisibleOverlayVisible:false', - }, { args: { copySubtitle: true }, expected: 'copyCurrentSubtitle' }, { args: { copySubtitleMultiple: true }, diff --git a/src/core/services/cli-command.ts b/src/core/services/cli-command.ts index 6f1902b..ff22bfd 100644 --- a/src/core/services/cli-command.ts +++ b/src/core/services/cli-command.ts @@ -16,10 +16,8 @@ export interface CliCommandServiceDeps { isOverlayRuntimeInitialized: () => boolean; initializeOverlayRuntime: () => void; toggleVisibleOverlay: () => void; - toggleInvisibleOverlay: () => void; openYomitanSettingsDelayed: (delayMs: number) => void; setVisibleOverlayVisible: (visible: boolean) => void; - setInvisibleOverlayVisible: (visible: boolean) => void; copyCurrentSubtitle: () => void; startPendingMultiCopy: (timeoutMs: number) => void; mineSentenceCard: () => Promise; @@ -93,9 +91,7 @@ interface OverlayCliRuntime { isInitialized: () => boolean; initialize: () => void; toggleVisible: () => void; - toggleInvisible: () => void; setVisible: (visible: boolean) => void; - setInvisible: (visible: boolean) => void; } interface MiningCliRuntime { @@ -180,14 +176,12 @@ export function createCliCommandDepsRuntime( isOverlayRuntimeInitialized: options.overlay.isInitialized, initializeOverlayRuntime: options.overlay.initialize, toggleVisibleOverlay: options.overlay.toggleVisible, - toggleInvisibleOverlay: options.overlay.toggleInvisible, openYomitanSettingsDelayed: (delayMs) => { options.schedule(() => { options.ui.openYomitanSettings(); }, delayMs); }, setVisibleOverlayVisible: options.overlay.setVisible, - setInvisibleOverlayVisible: options.overlay.setInvisible, copyCurrentSubtitle: options.mining.copyCurrentSubtitle, startPendingMultiCopy: options.mining.startPendingMultiCopy, mineSentenceCard: options.mining.mineSentenceCard, @@ -242,14 +236,11 @@ export function handleCliCommand( args.stop || args.toggle || args.toggleVisibleOverlay || - args.toggleInvisibleOverlay || args.settings || args.show || args.hide || args.showVisibleOverlay || args.hideVisibleOverlay || - args.showInvisibleOverlay || - args.hideInvisibleOverlay || args.copySubtitle || args.copySubtitleMultiple || args.mineSentence || @@ -286,10 +277,7 @@ export function handleCliCommand( } const shouldStart = - args.start || - args.toggle || - args.toggleVisibleOverlay || - args.toggleInvisibleOverlay; + args.start || args.toggle || args.toggleVisibleOverlay; const needsOverlayRuntime = commandNeedsOverlayRuntime(args); const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start; @@ -325,18 +313,12 @@ export function handleCliCommand( if (args.toggle || args.toggleVisibleOverlay) { deps.toggleVisibleOverlay(); - } else if (args.toggleInvisibleOverlay) { - deps.toggleInvisibleOverlay(); } else if (args.settings) { deps.openYomitanSettingsDelayed(1000); } else if (args.show || args.showVisibleOverlay) { deps.setVisibleOverlayVisible(true); } else if (args.hide || args.hideVisibleOverlay) { deps.setVisibleOverlayVisible(false); - } else if (args.showInvisibleOverlay) { - deps.setInvisibleOverlayVisible(true); - } else if (args.hideInvisibleOverlay) { - deps.setInvisibleOverlayVisible(false); } else if (args.copySubtitle) { deps.copyCurrentSubtitle(); } else if (args.copySubtitleMultiple) { diff --git a/src/core/services/field-grouping-overlay.test.ts b/src/core/services/field-grouping-overlay.test.ts index 47921f6..0572d79 100644 --- a/src/core/services/field-grouping-overlay.test.ts +++ b/src/core/services/field-grouping-overlay.test.ts @@ -19,11 +19,9 @@ test('createFieldGroupingOverlayRuntime sends overlay messages and sets restore }, }), getVisibleOverlayVisible: () => visible, - getInvisibleOverlayVisible: () => false, setVisibleOverlayVisible: (next) => { visible = next; }, - setInvisibleOverlayVisible: () => {}, getResolver: () => null, setResolver: () => {}, getRestoreVisibleOverlayOnModalClose: () => restore, @@ -44,9 +42,7 @@ test('createFieldGroupingOverlayRuntime callback cancels when send fails', async const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({ getMainWindow: () => null, getVisibleOverlayVisible: () => false, - getInvisibleOverlayVisible: () => false, setVisibleOverlayVisible: () => {}, - setInvisibleOverlayVisible: () => {}, getResolver: () => resolver, setResolver: (next: ((choice: KikuFieldGroupingChoice) => void) | null) => { resolver = next; @@ -87,12 +83,10 @@ test('createFieldGroupingOverlayRuntime callback restores hidden visible overlay const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({ getMainWindow: () => null, getVisibleOverlayVisible: () => visible, - getInvisibleOverlayVisible: () => false, setVisibleOverlayVisible: (nextVisible) => { visible = nextVisible; visibilityTransitions.push(nextVisible); }, - setInvisibleOverlayVisible: () => {}, getResolver: () => resolver as ((choice: KikuFieldGroupingChoice) => void) | null, setResolver: (nextResolver: ((choice: KikuFieldGroupingChoice) => void) | null) => { resolver = nextResolver; diff --git a/src/core/services/field-grouping-overlay.ts b/src/core/services/field-grouping-overlay.ts index df5feb5..60b9b12 100644 --- a/src/core/services/field-grouping-overlay.ts +++ b/src/core/services/field-grouping-overlay.ts @@ -11,9 +11,7 @@ interface WindowLike { export interface FieldGroupingOverlayRuntimeOptions { getMainWindow: () => WindowLike | null; getVisibleOverlayVisible: () => boolean; - getInvisibleOverlayVisible: () => boolean; setVisibleOverlayVisible: (visible: boolean) => void; - setInvisibleOverlayVisible: (visible: boolean) => void; getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; getRestoreVisibleOverlayOnModalClose: () => Set; @@ -65,9 +63,7 @@ export function createFieldGroupingOverlayRuntime( ) => Promise) => { return createFieldGroupingCallbackRuntime({ getVisibleOverlayVisible: options.getVisibleOverlayVisible, - getInvisibleOverlayVisible: options.getInvisibleOverlayVisible, setVisibleOverlayVisible: options.setVisibleOverlayVisible, - setInvisibleOverlayVisible: options.setInvisibleOverlayVisible, getResolver: options.getResolver, setResolver: options.setResolver, sendToVisibleOverlay, diff --git a/src/core/services/field-grouping.ts b/src/core/services/field-grouping.ts index 4d6d6ec..9ed3cc3 100644 --- a/src/core/services/field-grouping.ts +++ b/src/core/services/field-grouping.ts @@ -2,9 +2,7 @@ import { KikuFieldGroupingChoice, KikuFieldGroupingRequestData } from '../../typ export function createFieldGroupingCallback(options: { getVisibleOverlayVisible: () => boolean; - getInvisibleOverlayVisible: () => boolean; setVisibleOverlayVisible: (visible: boolean) => void; - setInvisibleOverlayVisible: (visible: boolean) => void; getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; sendRequestToVisibleOverlay: (data: KikuFieldGroupingRequestData) => boolean; @@ -22,7 +20,6 @@ export function createFieldGroupingCallback(options: { } const previousVisibleOverlay = options.getVisibleOverlayVisible(); - const previousInvisibleOverlay = options.getInvisibleOverlayVisible(); let settled = false; const finish = (choice: KikuFieldGroupingChoice): void => { @@ -36,9 +33,6 @@ export function createFieldGroupingCallback(options: { if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) { options.setVisibleOverlayVisible(false); } - if (options.getInvisibleOverlayVisible() !== previousInvisibleOverlay) { - options.setInvisibleOverlayVisible(previousInvisibleOverlay); - } }; options.setResolver(finish); diff --git a/src/core/services/ipc.test.ts b/src/core/services/ipc.test.ts index 0e6bbc8..3e70ebb 100644 --- a/src/core/services/ipc.test.ts +++ b/src/core/services/ipc.test.ts @@ -36,10 +36,8 @@ function createFakeIpcRegistrar(): { test('createIpcDepsRuntime wires AniList handlers', async () => { const calls: string[] = []; const deps = createIpcDepsRuntime({ - getInvisibleWindow: () => null, getMainWindow: () => null, getVisibleOverlayVisibility: () => false, - getInvisibleOverlayVisibility: () => false, onOverlayModalClosed: () => {}, openYomitanSettings: () => {}, quitApp: () => {}, @@ -47,7 +45,6 @@ test('createIpcDepsRuntime wires AniList handlers', async () => { tokenizeCurrentSubtitle: async () => null, getCurrentSubtitleRaw: () => '', getCurrentSubtitleAss: () => '', - getMpvSubtitleRenderMetrics: () => null, getSubtitlePosition: () => null, getSubtitleStyle: () => null, saveSubtitlePosition: () => {}, @@ -64,7 +61,6 @@ test('createIpcDepsRuntime wires AniList handlers', async () => { setRuntimeOption: () => ({ ok: true }), cycleRuntimeOption: () => ({ ok: true }), reportOverlayContentBounds: () => {}, - reportHoveredSubtitleToken: () => {}, getAnilistStatus: () => ({ tokenStatus: 'resolved' }), clearAnilistToken: () => { calls.push('clearAnilistToken'); @@ -101,20 +97,15 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () = const cycles: Array<{ id: string; direction: 1 | -1 }> = []; registerIpcHandlers( { - getInvisibleWindow: () => null, - isVisibleOverlayVisible: () => false, - setInvisibleIgnoreMouseEvents: () => {}, onOverlayModalClosed: () => {}, openYomitanSettings: () => {}, quitApp: () => {}, toggleDevTools: () => {}, getVisibleOverlayVisibility: () => false, toggleVisibleOverlay: () => {}, - getInvisibleOverlayVisibility: () => false, tokenizeCurrentSubtitle: async () => null, getCurrentSubtitleRaw: () => '', getCurrentSubtitleAss: () => '', - getMpvSubtitleRenderMetrics: () => null, getSubtitlePosition: () => null, getSubtitleStyle: () => null, saveSubtitlePosition: () => {}, @@ -138,7 +129,6 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () = return { ok: true }; }, reportOverlayContentBounds: () => {}, - reportHoveredSubtitleToken: () => {}, getAnilistStatus: () => ({}), clearAnilistToken: () => {}, openAnilistSetup: () => {}, @@ -176,25 +166,24 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () = test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => { const { registrar, handlers } = createFakeIpcRegistrar(); const saves: unknown[] = []; - const modals: unknown[] = []; + const closedModals: unknown[] = []; + const openedModals: unknown[] = []; registerIpcHandlers( { - getInvisibleWindow: () => null, - isVisibleOverlayVisible: () => false, - setInvisibleIgnoreMouseEvents: () => {}, onOverlayModalClosed: (modal) => { - modals.push(modal); + closedModals.push(modal); + }, + onOverlayModalOpened: (modal) => { + openedModals.push(modal); }, openYomitanSettings: () => {}, quitApp: () => {}, toggleDevTools: () => {}, getVisibleOverlayVisibility: () => false, toggleVisibleOverlay: () => {}, - getInvisibleOverlayVisibility: () => false, tokenizeCurrentSubtitle: async () => null, getCurrentSubtitleRaw: () => '', getCurrentSubtitleAss: () => '', - getMpvSubtitleRenderMetrics: () => null, getSubtitlePosition: () => null, getSubtitleStyle: () => null, saveSubtitlePosition: (position) => { @@ -214,7 +203,6 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => { setRuntimeOption: () => ({ ok: true }), cycleRuntimeOption: () => ({ ok: true }), reportOverlayContentBounds: () => {}, - reportHoveredSubtitleToken: () => {}, getAnilistStatus: () => ({}), clearAnilistToken: () => {}, openAnilistSetup: () => {}, @@ -228,11 +216,16 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => { handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 'bad' }); handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 }); assert.deepEqual(saves, [ - { yPercent: 42, invisibleOffsetXPx: undefined, invisibleOffsetYPx: undefined }, + { yPercent: 42 }, ]); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'kiku'); - assert.deepEqual(modals, ['subsync', 'kiku']); + assert.deepEqual(closedModals, ['subsync', 'kiku']); + + handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'bad'); + handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'subsync'); + handlers.on.get(IPC_CHANNELS.command.overlayModalOpened)!({}, 'runtime-options'); + assert.deepEqual(openedModals, ['subsync', 'runtime-options']); }); diff --git a/src/core/services/ipc.ts b/src/core/services/ipc.ts index 1aa8d91..76a8b89 100644 --- a/src/core/services/ipc.ts +++ b/src/core/services/ipc.ts @@ -19,20 +19,16 @@ import { } from '../../shared/ipc/validators'; export interface IpcServiceDeps { - getInvisibleWindow: () => WindowLike | null; - isVisibleOverlayVisible: () => boolean; - setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void; onOverlayModalClosed: (modal: OverlayHostedModal) => void; + onOverlayModalOpened?: (modal: OverlayHostedModal) => void; openYomitanSettings: () => void; quitApp: () => void; toggleDevTools: () => void; getVisibleOverlayVisibility: () => boolean; toggleVisibleOverlay: () => void; - getInvisibleOverlayVisibility: () => boolean; tokenizeCurrentSubtitle: () => Promise; getCurrentSubtitleRaw: () => string; getCurrentSubtitleAss: () => string; - getMpvSubtitleRenderMetrics: () => unknown; getSubtitlePosition: () => unknown; getSubtitleStyle: () => unknown; saveSubtitlePosition: (position: SubtitlePosition) => void; @@ -54,7 +50,6 @@ export interface IpcServiceDeps { setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown; reportOverlayContentBounds: (payload: unknown) => void; - reportHoveredSubtitleToken: (tokenIndex: number | null) => void; getAnilistStatus: () => unknown; clearAnilistToken: () => void; openAnilistSetup: () => void; @@ -91,18 +86,16 @@ interface IpcMainRegistrar { } export interface IpcDepsRuntimeOptions { - getInvisibleWindow: () => WindowLike | null; getMainWindow: () => WindowLike | null; getVisibleOverlayVisibility: () => boolean; - getInvisibleOverlayVisibility: () => boolean; onOverlayModalClosed: (modal: OverlayHostedModal) => void; + onOverlayModalOpened?: (modal: OverlayHostedModal) => void; openYomitanSettings: () => void; quitApp: () => void; toggleVisibleOverlay: () => void; tokenizeCurrentSubtitle: () => Promise; getCurrentSubtitleRaw: () => string; getCurrentSubtitleAss: () => string; - getMpvSubtitleRenderMetrics: () => unknown; getSubtitlePosition: () => unknown; getSubtitleStyle: () => unknown; saveSubtitlePosition: (position: SubtitlePosition) => void; @@ -119,7 +112,6 @@ export interface IpcDepsRuntimeOptions { setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown; reportOverlayContentBounds: (payload: unknown) => void; - reportHoveredSubtitleToken: (tokenIndex: number | null) => void; getAnilistStatus: () => unknown; clearAnilistToken: () => void; openAnilistSetup: () => void; @@ -130,14 +122,8 @@ export interface IpcDepsRuntimeOptions { export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcServiceDeps { return { - getInvisibleWindow: () => options.getInvisibleWindow(), - isVisibleOverlayVisible: options.getVisibleOverlayVisibility, - setInvisibleIgnoreMouseEvents: (ignore, eventsOptions) => { - const invisibleWindow = options.getInvisibleWindow(); - if (!invisibleWindow || invisibleWindow.isDestroyed()) return; - invisibleWindow.setIgnoreMouseEvents(ignore, eventsOptions); - }, onOverlayModalClosed: options.onOverlayModalClosed, + onOverlayModalOpened: options.onOverlayModalOpened, openYomitanSettings: options.openYomitanSettings, quitApp: options.quitApp, toggleDevTools: () => { @@ -147,11 +133,9 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService }, getVisibleOverlayVisibility: options.getVisibleOverlayVisibility, toggleVisibleOverlay: options.toggleVisibleOverlay, - getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility, tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle, getCurrentSubtitleRaw: options.getCurrentSubtitleRaw, getCurrentSubtitleAss: options.getCurrentSubtitleAss, - getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics, getSubtitlePosition: options.getSubtitlePosition, getSubtitleStyle: options.getSubtitleStyle, saveSubtitlePosition: options.saveSubtitlePosition, @@ -182,7 +166,6 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService setRuntimeOption: options.setRuntimeOption, cycleRuntimeOption: options.cycleRuntimeOption, reportOverlayContentBounds: options.reportOverlayContentBounds, - reportHoveredSubtitleToken: options.reportHoveredSubtitleToken, getAnilistStatus: options.getAnilistStatus, clearAnilistToken: options.clearAnilistToken, openAnilistSetup: options.openAnilistSetup, @@ -200,17 +183,7 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar const parsedOptions = parseOptionalForwardingOptions(options); const senderWindow = BrowserWindow.fromWebContents((event as IpcMainEvent).sender); if (senderWindow && !senderWindow.isDestroyed()) { - const invisibleWindow = deps.getInvisibleWindow(); - if ( - senderWindow === invisibleWindow && - deps.isVisibleOverlayVisible() && - invisibleWindow && - !invisibleWindow.isDestroyed() - ) { - deps.setInvisibleIgnoreMouseEvents(true, { forward: true }); - } else { - senderWindow.setIgnoreMouseEvents(ignore, parsedOptions); - } + senderWindow.setIgnoreMouseEvents(ignore, parsedOptions); } }, ); @@ -220,6 +193,12 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar if (!parsedModal) return; deps.onOverlayModalClosed(parsedModal); }); + ipc.on(IPC_CHANNELS.command.overlayModalOpened, (_event: unknown, modal: unknown) => { + const parsedModal = parseOverlayHostedModal(modal); + if (!parsedModal) return; + if (!deps.onOverlayModalOpened) return; + deps.onOverlayModalOpened(parsedModal); + }); ipc.on(IPC_CHANNELS.command.openYomitanSettings, () => { deps.openYomitanSettings(); @@ -245,10 +224,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar return deps.getVisibleOverlayVisibility(); }); - ipc.handle(IPC_CHANNELS.request.getInvisibleOverlayVisibility, () => { - return deps.getInvisibleOverlayVisibility(); - }); - ipc.handle(IPC_CHANNELS.request.getCurrentSubtitle, async () => { return await deps.tokenizeCurrentSubtitle(); }); @@ -261,10 +236,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar return deps.getCurrentSubtitleAss(); }); - ipc.handle(IPC_CHANNELS.request.getMpvSubtitleRenderMetrics, () => { - return deps.getMpvSubtitleRenderMetrics(); - }); - ipc.handle(IPC_CHANNELS.request.getSubtitlePosition, () => { return deps.getSubtitlePosition(); }); @@ -358,17 +329,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar deps.reportOverlayContentBounds(payload); }); - ipc.on('subtitle-token-hover:set', (_event: unknown, tokenIndex: unknown) => { - if (tokenIndex === null) { - deps.reportHoveredSubtitleToken(null); - return; - } - if (!Number.isInteger(tokenIndex) || (tokenIndex as number) < 0) { - return; - } - deps.reportHoveredSubtitleToken(tokenIndex as number); - }); - ipc.handle(IPC_CHANNELS.request.getAnilistStatus, () => { return deps.getAnilistStatus(); }); diff --git a/src/core/services/overlay-bridge.test.ts b/src/core/services/overlay-bridge.test.ts index 3a46d15..5b1cb6b 100644 --- a/src/core/services/overlay-bridge.test.ts +++ b/src/core/services/overlay-bridge.test.ts @@ -33,13 +33,51 @@ test('sendToVisibleOverlayRuntime restores visibility flag when opening hidden o assert.deepEqual(sent, [['runtime-options:open']]); }); +test('sendToVisibleOverlayRuntime waits for overlay page before sending open command', () => { + const sent: unknown[][] = []; + const restoreSet = new Set<'runtime-options' | 'subsync'>(); + let loading = true; + let currentURL = ''; + const finishCallbacks: Array<() => void> = []; + + const ok = sendToVisibleOverlayRuntime({ + mainWindow: { + isDestroyed: () => false, + webContents: { + isLoading: () => loading, + getURL: () => currentURL, + send: (...args: unknown[]) => { + sent.push(args); + }, + once: (_event: string, callback: () => void) => { + finishCallbacks.push(callback); + }, + }, + } as unknown as Electron.BrowserWindow, + visibleOverlayVisible: false, + setVisibleOverlayVisible: () => {}, + channel: 'runtime-options:open', + restoreOnModalClose: 'runtime-options', + restoreVisibleOverlayOnModalClose: restoreSet, + }); + + assert.equal(ok, true); + assert.deepEqual(sent, []); + assert.equal(restoreSet.has('runtime-options'), true); + + loading = false; + currentURL = 'file:///overlay/index.html?layer=visible'; + assert.ok(finishCallbacks[0]); + finishCallbacks[0]!(); + + assert.deepEqual(sent, [['runtime-options:open']]); +}); + test('createFieldGroupingCallbackRuntime cancels when overlay request cannot be sent', async () => { let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null; const callback = createFieldGroupingCallbackRuntime<'runtime-options' | 'subsync'>({ getVisibleOverlayVisible: () => false, - getInvisibleOverlayVisible: () => false, setVisibleOverlayVisible: () => {}, - setInvisibleOverlayVisible: () => {}, getResolver: () => resolver, setResolver: (next) => { resolver = next; diff --git a/src/core/services/overlay-bridge.ts b/src/core/services/overlay-bridge.ts index 0326b3b..a0461be 100644 --- a/src/core/services/overlay-bridge.ts +++ b/src/core/services/overlay-bridge.ts @@ -26,27 +26,32 @@ export function sendToVisibleOverlayRuntime(options: { options.mainWindow!.webContents.send(options.channel, options.payload); } }; - if (options.mainWindow.webContents.isLoading()) { + + const getURL = options.mainWindow.webContents.getURL; + const currentURL = + typeof getURL === 'function' ? getURL.call(options.mainWindow.webContents) : 'ready'; + const isReady = + !options.mainWindow.webContents.isLoading() && + currentURL !== '' && + currentURL !== 'about:blank'; + + if (!isReady) { options.mainWindow.webContents.once('did-finish-load', () => { - if ( - options.mainWindow && - !options.mainWindow.isDestroyed() && - !options.mainWindow.webContents.isLoading() - ) { + if (!options.mainWindow || options.mainWindow.isDestroyed()) return; + if (!options.mainWindow.webContents.isLoading()) { sendNow(); } }); return true; } + sendNow(); return true; } export function createFieldGroupingCallbackRuntime(options: { getVisibleOverlayVisible: () => boolean; - getInvisibleOverlayVisible: () => boolean; setVisibleOverlayVisible: (visible: boolean) => void; - setInvisibleOverlayVisible: (visible: boolean) => void; getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; sendToVisibleOverlay: ( @@ -57,9 +62,7 @@ export function createFieldGroupingCallbackRuntime(options: { }): (data: KikuFieldGroupingRequestData) => Promise { return createFieldGroupingCallback({ getVisibleOverlayVisible: options.getVisibleOverlayVisible, - getInvisibleOverlayVisible: options.getInvisibleOverlayVisible, setVisibleOverlayVisible: options.setVisibleOverlayVisible, - setInvisibleOverlayVisible: options.setInvisibleOverlayVisible, getResolver: options.getResolver, setResolver: options.setResolver, sendRequestToVisibleOverlay: (data) => diff --git a/src/core/services/overlay-content-measurement.test.ts b/src/core/services/overlay-content-measurement.test.ts index 43e0fa4..8854112 100644 --- a/src/core/services/overlay-content-measurement.test.ts +++ b/src/core/services/overlay-content-measurement.test.ts @@ -28,7 +28,7 @@ test('sanitizeOverlayContentMeasurement accepts valid payload with null rect', ( test('sanitizeOverlayContentMeasurement rejects invalid ranges', () => { const measurement = sanitizeOverlayContentMeasurement( { - layer: 'invisible', + layer: 'visible', measuredAtMs: 100, viewport: { width: 0, height: 1080 }, contentRect: { x: 0, y: 0, width: 100, height: 20 }, @@ -39,7 +39,7 @@ test('sanitizeOverlayContentMeasurement rejects invalid ranges', () => { assert.equal(measurement, null); }); -test('overlay measurement store keeps latest payload per layer', () => { +test('overlay measurement store keeps latest payload for visible layer', () => { const store = createOverlayContentMeasurementStore({ now: () => 1000, warn: () => { @@ -53,17 +53,9 @@ test('overlay measurement store keeps latest payload per layer', () => { viewport: { width: 1280, height: 720 }, contentRect: { x: 50, y: 60, width: 400, height: 80 }, }); - const invisible = store.report({ - layer: 'invisible', - measuredAtMs: 910, - viewport: { width: 1280, height: 720 }, - contentRect: { x: 20, y: 30, width: 300, height: 40 }, - }); assert.equal(visible?.layer, 'visible'); - assert.equal(invisible?.layer, 'invisible'); assert.equal(store.getLatestByLayer('visible')?.contentRect?.width, 400); - assert.equal(store.getLatestByLayer('invisible')?.contentRect?.height, 40); }); test('overlay measurement store rate-limits invalid payload warnings', () => { diff --git a/src/core/services/overlay-content-measurement.ts b/src/core/services/overlay-content-measurement.ts index 4e86823..5631cce 100644 --- a/src/core/services/overlay-content-measurement.ts +++ b/src/core/services/overlay-content-measurement.ts @@ -28,7 +28,7 @@ export function sanitizeOverlayContentMeasurement( } | null; }; - if (candidate.layer !== 'visible' && candidate.layer !== 'invisible') { + if (candidate.layer !== 'visible') { return null; } @@ -112,7 +112,6 @@ export function createOverlayContentMeasurementStore(options?: { const warn = options?.warn ?? ((message: string) => logger.warn(message)); const latestByLayer: OverlayMeasurementStore = { visible: null, - invisible: null, }; let droppedInvalid = 0; diff --git a/src/core/services/overlay-runtime-init.ts b/src/core/services/overlay-runtime-init.ts index 6bd1db2..a055266 100644 --- a/src/core/services/overlay-runtime-init.ts +++ b/src/core/services/overlay-runtime-init.ts @@ -10,16 +10,11 @@ import { export function initializeOverlayRuntime(options: { backendOverride: string | null; - getInitialInvisibleOverlayVisibility: () => boolean; createMainWindow: () => void; - createInvisibleWindow: () => void; registerGlobalShortcuts: () => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; isVisibleOverlayVisible: () => boolean; - isInvisibleOverlayVisible: () => boolean; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; getOverlayWindows: () => BrowserWindow[]; syncOverlayShortcuts: () => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void; @@ -38,12 +33,8 @@ export function initializeOverlayRuntime(options: { data: KikuFieldGroupingRequestData, ) => Promise; getKnownWordCacheStatePath: () => string; -}): { - invisibleOverlayVisible: boolean; -} { +}): void { options.createMainWindow(); - options.createInvisibleWindow(); - const invisibleOverlayVisible = options.getInitialInvisibleOverlayVisibility(); options.registerGlobalShortcuts(); const windowTracker = createWindowTracker(options.backendOverride, options.getMpvSocketPath()); @@ -51,17 +42,12 @@ export function initializeOverlayRuntime(options: { if (windowTracker) { windowTracker.onGeometryChange = (geometry: WindowGeometry) => { options.updateVisibleOverlayBounds(geometry); - options.updateInvisibleOverlayBounds(geometry); }; windowTracker.onWindowFound = (geometry: WindowGeometry) => { options.updateVisibleOverlayBounds(geometry); - options.updateInvisibleOverlayBounds(geometry); if (options.isVisibleOverlayVisible()) { options.updateVisibleOverlayVisibility(); } - if (options.isInvisibleOverlayVisible()) { - options.updateInvisibleOverlayVisibility(); - } }; windowTracker.onWindowLost = () => { for (const window of options.getOverlayWindows()) { @@ -101,7 +87,4 @@ export function initializeOverlayRuntime(options: { } options.updateVisibleOverlayVisibility(); - options.updateInvisibleOverlayVisibility(); - - return { invisibleOverlayVisible }; } diff --git a/src/core/services/overlay-shortcut-handler.test.ts b/src/core/services/overlay-shortcut-handler.test.ts index 72a5a5c..6570138 100644 --- a/src/core/services/overlay-shortcut-handler.test.ts +++ b/src/core/services/overlay-shortcut-handler.test.ts @@ -10,7 +10,6 @@ import { function makeShortcuts(overrides: Partial = {}): ConfiguredShortcuts { return { toggleVisibleOverlayGlobal: null, - toggleInvisibleOverlayGlobal: null, copySubtitle: null, copySubtitleMultiple: null, updateLastCardFromClipboard: null, diff --git a/src/core/services/overlay-visibility.test.ts b/src/core/services/overlay-visibility.test.ts new file mode 100644 index 0000000..e104c42 --- /dev/null +++ b/src/core/services/overlay-visibility.test.ts @@ -0,0 +1,265 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility'; + +type WindowTrackerStub = { + isTracking: () => boolean; + getGeometry: () => { x: number; y: number; width: number; height: number } | null; +}; + +function createMainWindowRecorder() { + const calls: string[] = []; + const window = { + isDestroyed: () => false, + hide: () => { + calls.push('hide'); + }, + show: () => { + calls.push('show'); + }, + focus: () => { + calls.push('focus'); + }, + setIgnoreMouseEvents: () => { + calls.push('mouse-ignore'); + }, + }; + + return { window, calls }; +} + +test('macOS keeps visible overlay hidden while tracker is not ready and emits one loading OSD', () => { + const { window, calls } = createMainWindowRecorder(); + let trackerWarning = false; + const osdMessages: string[] = []; + const tracker: WindowTrackerStub = { + isTracking: () => false, + getGeometry: () => null, + }; + + const run = () => + updateVisibleOverlayVisibility({ + visibleOverlayVisible: true, + mainWindow: window as never, + windowTracker: tracker as never, + trackerNotReadyWarningShown: trackerWarning, + setTrackerNotReadyWarningShown: (shown: boolean) => { + trackerWarning = shown; + }, + updateVisibleOverlayBounds: () => { + calls.push('update-bounds'); + }, + ensureOverlayWindowLevel: () => { + calls.push('ensure-level'); + }, + syncPrimaryOverlayWindowLayer: () => { + calls.push('sync-layer'); + }, + enforceOverlayLayerOrder: () => { + calls.push('enforce-order'); + }, + syncOverlayShortcuts: () => { + calls.push('sync-shortcuts'); + }, + isMacOSPlatform: true, + showOverlayLoadingOsd: (message: string) => { + osdMessages.push(message); + }, + } as never); + + run(); + run(); + + assert.equal(trackerWarning, true); + assert.deepEqual(osdMessages, ['Overlay loading...']); + assert.ok(calls.includes('hide')); + assert.ok(!calls.includes('show')); +}); + +test('non-macOS keeps fallback visible overlay behavior when tracker is not ready', () => { + const { window, calls } = createMainWindowRecorder(); + let trackerWarning = false; + const tracker: WindowTrackerStub = { + isTracking: () => false, + getGeometry: () => null, + }; + + updateVisibleOverlayVisibility({ + visibleOverlayVisible: true, + mainWindow: window as never, + windowTracker: tracker as never, + trackerNotReadyWarningShown: trackerWarning, + setTrackerNotReadyWarningShown: (shown: boolean) => { + trackerWarning = shown; + }, + updateVisibleOverlayBounds: () => { + calls.push('update-bounds'); + }, + ensureOverlayWindowLevel: () => { + calls.push('ensure-level'); + }, + syncPrimaryOverlayWindowLayer: () => { + calls.push('sync-layer'); + }, + enforceOverlayLayerOrder: () => { + calls.push('enforce-order'); + }, + syncOverlayShortcuts: () => { + calls.push('sync-shortcuts'); + }, + isMacOSPlatform: false, + showOverlayLoadingOsd: () => { + calls.push('osd'); + }, + resolveFallbackBounds: () => ({ x: 12, y: 24, width: 640, height: 360 }), + } as never); + + assert.equal(trackerWarning, true); + assert.ok(calls.includes('update-bounds')); + assert.ok(calls.includes('show')); + assert.ok(calls.includes('focus')); + assert.ok(!calls.includes('osd')); +}); + +test('macOS keeps visible overlay hidden while tracker is not initialized yet', () => { + const { window, calls } = createMainWindowRecorder(); + let trackerWarning = false; + const osdMessages: string[] = []; + + updateVisibleOverlayVisibility({ + visibleOverlayVisible: true, + mainWindow: window as never, + windowTracker: null, + trackerNotReadyWarningShown: trackerWarning, + setTrackerNotReadyWarningShown: (shown: boolean) => { + trackerWarning = shown; + }, + updateVisibleOverlayBounds: () => { + calls.push('update-bounds'); + }, + ensureOverlayWindowLevel: () => { + calls.push('ensure-level'); + }, + syncPrimaryOverlayWindowLayer: () => { + calls.push('sync-layer'); + }, + enforceOverlayLayerOrder: () => { + calls.push('enforce-order'); + }, + syncOverlayShortcuts: () => { + calls.push('sync-shortcuts'); + }, + isMacOSPlatform: true, + showOverlayLoadingOsd: (message: string) => { + osdMessages.push(message); + }, + } as never); + + assert.equal(trackerWarning, true); + assert.deepEqual(osdMessages, ['Overlay loading...']); + assert.ok(calls.includes('hide')); + assert.ok(!calls.includes('show')); + assert.ok(!calls.includes('update-bounds')); +}); + +test('setVisibleOverlayVisible does not mutate mpv subtitle visibility directly', () => { + const calls: string[] = []; + setVisibleOverlayVisible({ + visible: true, + setVisibleOverlayVisibleState: (visible) => { + calls.push(`state:${visible}`); + }, + updateVisibleOverlayVisibility: () => { + calls.push('update'); + }, + }); + + assert.deepEqual(calls, ['state:true', 'update']); +}); + +test('macOS loading OSD can show again after overlay is hidden and retried', () => { + const { window, calls } = createMainWindowRecorder(); + const osdMessages: string[] = []; + let trackerWarning = false; + + updateVisibleOverlayVisibility({ + visibleOverlayVisible: true, + mainWindow: window as never, + windowTracker: null, + trackerNotReadyWarningShown: trackerWarning, + setTrackerNotReadyWarningShown: (shown: boolean) => { + trackerWarning = shown; + calls.push(`warn:${shown ? 'yes' : 'no'}`); + }, + updateVisibleOverlayBounds: () => { + calls.push('update-bounds'); + }, + ensureOverlayWindowLevel: () => { + calls.push('ensure-level'); + }, + syncPrimaryOverlayWindowLayer: () => { + calls.push('sync-layer'); + }, + enforceOverlayLayerOrder: () => { + calls.push('enforce-order'); + }, + syncOverlayShortcuts: () => { + calls.push('sync-shortcuts'); + }, + isMacOSPlatform: true, + showOverlayLoadingOsd: (message: string) => { + osdMessages.push(message); + }, + } as never); + + updateVisibleOverlayVisibility({ + visibleOverlayVisible: false, + mainWindow: window as never, + windowTracker: null, + trackerNotReadyWarningShown: trackerWarning, + setTrackerNotReadyWarningShown: (shown: boolean) => { + trackerWarning = shown; + calls.push(`warn:${shown ? 'yes' : 'no'}`); + }, + updateVisibleOverlayBounds: () => {}, + ensureOverlayWindowLevel: () => {}, + syncPrimaryOverlayWindowLayer: () => {}, + enforceOverlayLayerOrder: () => {}, + syncOverlayShortcuts: () => {}, + isMacOSPlatform: true, + showOverlayLoadingOsd: () => {}, + } as never); + + updateVisibleOverlayVisibility({ + visibleOverlayVisible: true, + mainWindow: window as never, + windowTracker: null, + trackerNotReadyWarningShown: trackerWarning, + setTrackerNotReadyWarningShown: (shown: boolean) => { + trackerWarning = shown; + calls.push(`warn:${shown ? 'yes' : 'no'}`); + }, + updateVisibleOverlayBounds: () => { + calls.push('update-bounds'); + }, + ensureOverlayWindowLevel: () => { + calls.push('ensure-level'); + }, + syncPrimaryOverlayWindowLayer: () => { + calls.push('sync-layer'); + }, + enforceOverlayLayerOrder: () => { + calls.push('enforce-order'); + }, + syncOverlayShortcuts: () => { + calls.push('sync-shortcuts'); + }, + isMacOSPlatform: true, + showOverlayLoadingOsd: (message: string) => { + osdMessages.push(message); + }, + } as never); + + assert.deepEqual(osdMessages, ['Overlay loading...', 'Overlay loading...']); +}); diff --git a/src/core/services/overlay-visibility.ts b/src/core/services/overlay-visibility.ts index a0f0117..96cea34 100644 --- a/src/core/services/overlay-visibility.ts +++ b/src/core/services/overlay-visibility.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, screen } from 'electron'; +import type { BrowserWindow } from 'electron'; import { BaseWindowTracker } from '../../window-trackers'; import { WindowGeometry } from '../../types'; @@ -10,14 +10,19 @@ export function updateVisibleOverlayVisibility(args: { setTrackerNotReadyWarningShown: (shown: boolean) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; ensureOverlayWindowLevel: (window: BrowserWindow) => void; + syncPrimaryOverlayWindowLayer: (layer: 'visible') => void; enforceOverlayLayerOrder: () => void; syncOverlayShortcuts: () => void; + isMacOSPlatform?: boolean; + showOverlayLoadingOsd?: (message: string) => void; + resolveFallbackBounds: () => WindowGeometry; }): void { if (!args.mainWindow || args.mainWindow.isDestroyed()) { return; } if (!args.visibleOverlayVisible) { + args.setTrackerNotReadyWarningShown(false); args.mainWindow.hide(); args.syncOverlayShortcuts(); return; @@ -29,6 +34,8 @@ export function updateVisibleOverlayVisibility(args: { if (geometry) { args.updateVisibleOverlayBounds(geometry); } + args.syncPrimaryOverlayWindowLayer('visible'); + args.mainWindow.setIgnoreMouseEvents(false); args.ensureOverlayWindowLevel(args.mainWindow); args.mainWindow.show(); args.mainWindow.focus(); @@ -38,7 +45,18 @@ export function updateVisibleOverlayVisibility(args: { } if (!args.windowTracker) { + if (args.isMacOSPlatform) { + if (!args.trackerNotReadyWarningShown) { + args.setTrackerNotReadyWarningShown(true); + args.showOverlayLoadingOsd?.('Overlay loading...'); + } + args.mainWindow.hide(); + args.syncOverlayShortcuts(); + return; + } args.setTrackerNotReadyWarningShown(false); + args.syncPrimaryOverlayWindowLayer('visible'); + args.mainWindow.setIgnoreMouseEvents(false); args.ensureOverlayWindowLevel(args.mainWindow); args.mainWindow.show(); args.mainWindow.focus(); @@ -49,16 +67,21 @@ export function updateVisibleOverlayVisibility(args: { if (!args.trackerNotReadyWarningShown) { args.setTrackerNotReadyWarningShown(true); + if (args.isMacOSPlatform) { + args.showOverlayLoadingOsd?.('Overlay loading...'); + } } - const cursorPoint = screen.getCursorScreenPoint(); - const display = screen.getDisplayNearestPoint(cursorPoint); - const fallbackBounds = display.workArea; - args.updateVisibleOverlayBounds({ - x: fallbackBounds.x, - y: fallbackBounds.y, - width: fallbackBounds.width, - height: fallbackBounds.height, - }); + + if (args.isMacOSPlatform) { + args.mainWindow.hide(); + args.syncOverlayShortcuts(); + return; + } + + const fallbackBounds = args.resolveFallbackBounds(); + args.updateVisibleOverlayBounds(fallbackBounds); + args.syncPrimaryOverlayWindowLayer('visible'); + args.mainWindow.setIgnoreMouseEvents(false); args.ensureOverlayWindowLevel(args.mainWindow); args.mainWindow.show(); args.mainWindow.focus(); @@ -66,111 +89,11 @@ export function updateVisibleOverlayVisibility(args: { args.syncOverlayShortcuts(); } -export function updateInvisibleOverlayVisibility(args: { - invisibleWindow: BrowserWindow | null; - visibleOverlayVisible: boolean; - invisibleOverlayVisible: boolean; - windowTracker: BaseWindowTracker | null; - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; - ensureOverlayWindowLevel: (window: BrowserWindow) => void; - enforceOverlayLayerOrder: () => void; - syncOverlayShortcuts: () => void; -}): void { - if (!args.invisibleWindow || args.invisibleWindow.isDestroyed()) { - return; - } - - if (args.visibleOverlayVisible) { - args.invisibleWindow.hide(); - args.syncOverlayShortcuts(); - return; - } - - const showInvisibleWithoutFocus = (): void => { - args.ensureOverlayWindowLevel(args.invisibleWindow!); - if (typeof args.invisibleWindow!.showInactive === 'function') { - args.invisibleWindow!.showInactive(); - } else { - args.invisibleWindow!.show(); - } - args.enforceOverlayLayerOrder(); - }; - - if (!args.invisibleOverlayVisible) { - args.invisibleWindow.hide(); - args.syncOverlayShortcuts(); - return; - } - - if (args.windowTracker && args.windowTracker.isTracking()) { - const geometry = args.windowTracker.getGeometry(); - if (geometry) { - args.updateInvisibleOverlayBounds(geometry); - } - showInvisibleWithoutFocus(); - args.syncOverlayShortcuts(); - return; - } - - if (!args.windowTracker) { - showInvisibleWithoutFocus(); - args.syncOverlayShortcuts(); - return; - } - - const cursorPoint = screen.getCursorScreenPoint(); - const display = screen.getDisplayNearestPoint(cursorPoint); - const fallbackBounds = display.workArea; - args.updateInvisibleOverlayBounds({ - x: fallbackBounds.x, - y: fallbackBounds.y, - width: fallbackBounds.width, - height: fallbackBounds.height, - }); - showInvisibleWithoutFocus(); - args.syncOverlayShortcuts(); -} - -export function syncInvisibleOverlayMousePassthrough(options: { - hasInvisibleWindow: () => boolean; - setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void; - visibleOverlayVisible: boolean; - invisibleOverlayVisible: boolean; -}): void { - if (!options.hasInvisibleWindow()) return; - if (options.visibleOverlayVisible) { - options.setIgnoreMouseEvents(true, { forward: true }); - } else if (options.invisibleOverlayVisible) { - options.setIgnoreMouseEvents(false); - } -} - export function setVisibleOverlayVisible(options: { visible: boolean; setVisibleOverlayVisibleState: (visible: boolean) => void; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; - syncInvisibleOverlayMousePassthrough: () => void; - shouldBindVisibleOverlayToMpvSubVisibility: () => boolean; - isMpvConnected: () => boolean; - setMpvSubVisibility: (visible: boolean) => void; }): void { options.setVisibleOverlayVisibleState(options.visible); options.updateVisibleOverlayVisibility(); - options.updateInvisibleOverlayVisibility(); - options.syncInvisibleOverlayMousePassthrough(); - if (options.shouldBindVisibleOverlayToMpvSubVisibility() && options.isMpvConnected()) { - options.setMpvSubVisibility(!options.visible); - } -} - -export function setInvisibleOverlayVisible(options: { - visible: boolean; - setInvisibleOverlayVisibleState: (visible: boolean) => void; - updateInvisibleOverlayVisibility: () => void; - syncInvisibleOverlayMousePassthrough: () => void; -}): void { - options.setInvisibleOverlayVisibleState(options.visible); - options.updateInvisibleOverlayVisibility(); - options.syncInvisibleOverlayMousePassthrough(); } diff --git a/src/core/services/runtime-config.test.ts b/src/core/services/runtime-config.test.ts index 6726638..88d007d 100644 --- a/src/core/services/runtime-config.test.ts +++ b/src/core/services/runtime-config.test.ts @@ -1,7 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { - getInitialInvisibleOverlayVisibility, isAutoUpdateEnabledRuntime, shouldAutoInitializeOverlayRuntimeFromConfig, shouldBindVisibleOverlayToMpvSubVisibility, @@ -10,9 +9,6 @@ import { const BASE_CONFIG = { auto_start_overlay: false, bind_visible_overlay_to_mpv_sub_visibility: true, - invisibleOverlay: { - startupVisibility: 'platform-default' as const, - }, ankiConnect: { behavior: { autoUpdateNewCards: true, @@ -20,26 +16,7 @@ const BASE_CONFIG = { }, }; -test('getInitialInvisibleOverlayVisibility handles visibility + platform', () => { - assert.equal( - getInitialInvisibleOverlayVisibility( - { ...BASE_CONFIG, invisibleOverlay: { startupVisibility: 'visible' } }, - 'linux', - ), - true, - ); - assert.equal( - getInitialInvisibleOverlayVisibility( - { ...BASE_CONFIG, invisibleOverlay: { startupVisibility: 'hidden' } }, - 'darwin', - ), - false, - ); - assert.equal(getInitialInvisibleOverlayVisibility(BASE_CONFIG, 'linux'), false); - assert.equal(getInitialInvisibleOverlayVisibility(BASE_CONFIG, 'darwin'), true); -}); - -test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start and visible startup', () => { +test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start', () => { assert.equal(shouldAutoInitializeOverlayRuntimeFromConfig(BASE_CONFIG), false); assert.equal( shouldAutoInitializeOverlayRuntimeFromConfig({ @@ -48,13 +25,6 @@ test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start and visib }), true, ); - assert.equal( - shouldAutoInitializeOverlayRuntimeFromConfig({ - ...BASE_CONFIG, - invisibleOverlay: { startupVisibility: 'visible' }, - }), - true, - ); }); test('shouldBindVisibleOverlayToMpvSubVisibility returns config value', () => { diff --git a/src/core/services/shortcut.ts b/src/core/services/shortcut.ts index 76e9a18..b9c4a1e 100644 --- a/src/core/services/shortcut.ts +++ b/src/core/services/shortcut.ts @@ -5,14 +5,12 @@ const logger = createLogger('main:shortcut'); export interface GlobalShortcutConfig { toggleVisibleOverlayGlobal: string | null | undefined; - toggleInvisibleOverlayGlobal: string | null | undefined; openJimaku?: string | null | undefined; } export interface RegisterGlobalShortcutsServiceOptions { shortcuts: GlobalShortcutConfig; onToggleVisibleOverlay: () => void; - onToggleInvisibleOverlay: () => void; onOpenYomitanSettings: () => void; onOpenJimaku?: () => void; isDev: boolean; @@ -21,9 +19,7 @@ export interface RegisterGlobalShortcutsServiceOptions { export function registerGlobalShortcuts(options: RegisterGlobalShortcutsServiceOptions): void { const visibleShortcut = options.shortcuts.toggleVisibleOverlayGlobal; - const invisibleShortcut = options.shortcuts.toggleInvisibleOverlayGlobal; const normalizedVisible = visibleShortcut?.replace(/\s+/g, '').toLowerCase(); - const normalizedInvisible = invisibleShortcut?.replace(/\s+/g, '').toLowerCase(); const normalizedJimaku = options.shortcuts.openJimaku?.replace(/\s+/g, '').toLowerCase(); const normalizedSettings = 'alt+shift+y'; @@ -38,31 +34,10 @@ export function registerGlobalShortcuts(options: RegisterGlobalShortcutsServiceO } } - if (invisibleShortcut && normalizedInvisible && normalizedInvisible !== normalizedVisible) { - const toggleInvisibleRegistered = globalShortcut.register(invisibleShortcut, () => { - options.onToggleInvisibleOverlay(); - }); - if (!toggleInvisibleRegistered) { - logger.warn( - `Failed to register global shortcut toggleInvisibleOverlayGlobal: ${invisibleShortcut}`, - ); - } - } else if ( - invisibleShortcut && - normalizedInvisible && - normalizedInvisible === normalizedVisible - ) { - logger.warn( - 'Skipped registering toggleInvisibleOverlayGlobal because it collides with toggleVisibleOverlayGlobal', - ); - } - if (options.shortcuts.openJimaku && options.onOpenJimaku) { if ( normalizedJimaku && - (normalizedJimaku === normalizedVisible || - normalizedJimaku === normalizedInvisible || - normalizedJimaku === normalizedSettings) + (normalizedJimaku === normalizedVisible || normalizedJimaku === normalizedSettings) ) { logger.warn( 'Skipped registering openJimaku because it collides with another global shortcut', diff --git a/src/core/services/startup-bootstrap.test.ts b/src/core/services/startup-bootstrap.test.ts index db7d24f..dff7c40 100644 --- a/src/core/services/startup-bootstrap.test.ts +++ b/src/core/services/startup-bootstrap.test.ts @@ -10,14 +10,11 @@ function makeArgs(overrides: Partial = {}): CliArgs { stop: false, toggle: false, toggleVisibleOverlay: false, - toggleInvisibleOverlay: false, settings: false, show: false, hide: false, showVisibleOverlay: false, hideVisibleOverlay: false, - showInvisibleOverlay: false, - hideInvisibleOverlay: false, copySubtitle: false, copySubtitleMultiple: false, mineSentence: false, diff --git a/src/core/services/startup.ts b/src/core/services/startup.ts index 7b14dc1..ebc9835 100644 --- a/src/core/services/startup.ts +++ b/src/core/services/startup.ts @@ -19,9 +19,6 @@ interface RuntimeAutoUpdateOptionManagerLike { export interface RuntimeConfigLike { auto_start_overlay?: boolean; bind_visible_overlay_to_mpv_sub_visibility: boolean; - invisibleOverlay: { - startupVisibility: 'visible' | 'hidden' | 'platform-default'; - }; ankiConnect?: { behavior?: { autoUpdateNewCards?: boolean; @@ -155,21 +152,8 @@ function getStartupCriticalConfigErrors(config: AppReadyConfigLike): string[] { return errors; } -export function getInitialInvisibleOverlayVisibility( - config: RuntimeConfigLike, - platform: NodeJS.Platform, -): boolean { - const visibility = config.invisibleOverlay.startupVisibility; - if (visibility === 'visible') return true; - if (visibility === 'hidden') return false; - if (platform === 'linux') return false; - return true; -} - export function shouldAutoInitializeOverlayRuntimeFromConfig(config: RuntimeConfigLike): boolean { - if (config.auto_start_overlay === true) return true; - if (config.invisibleOverlay.startupVisibility === 'visible') return true; - return false; + return config.auto_start_overlay === true; } export function shouldBindVisibleOverlayToMpvSubVisibility(config: RuntimeConfigLike): boolean { diff --git a/src/core/services/subtitle-position.ts b/src/core/services/subtitle-position.ts index 9974367..b3a8bdd 100644 --- a/src/core/services/subtitle-position.ts +++ b/src/core/services/subtitle-position.ts @@ -101,20 +101,7 @@ export function loadSubtitlePosition( const data = fs.readFileSync(positionPath, 'utf-8'); const parsed = JSON.parse(data) as Partial; if (parsed && typeof parsed.yPercent === 'number' && Number.isFinite(parsed.yPercent)) { - const position: SubtitlePosition = { yPercent: parsed.yPercent }; - if ( - typeof parsed.invisibleOffsetXPx === 'number' && - Number.isFinite(parsed.invisibleOffsetXPx) - ) { - position.invisibleOffsetXPx = parsed.invisibleOffsetXPx; - } - if ( - typeof parsed.invisibleOffsetYPx === 'number' && - Number.isFinite(parsed.invisibleOffsetYPx) - ) { - position.invisibleOffsetYPx = parsed.invisibleOffsetYPx; - } - return position; + return { yPercent: parsed.yPercent }; } return options.fallbackPosition; } catch (err) { diff --git a/src/core/utils/shortcut-config.ts b/src/core/utils/shortcut-config.ts index f7e7c5b..991c195 100644 --- a/src/core/utils/shortcut-config.ts +++ b/src/core/utils/shortcut-config.ts @@ -2,7 +2,6 @@ import { Config } from '../../types'; export interface ConfiguredShortcuts { toggleVisibleOverlayGlobal: string | null | undefined; - toggleInvisibleOverlayGlobal: string | null | undefined; copySubtitle: string | null | undefined; copySubtitleMultiple: string | null | undefined; updateLastCardFromClipboard: string | null | undefined; @@ -33,10 +32,6 @@ export function resolveConfiguredShortcuts( config.shortcuts?.toggleVisibleOverlayGlobal ?? defaultConfig.shortcuts?.toggleVisibleOverlayGlobal, ), - toggleInvisibleOverlayGlobal: normalizeShortcut( - config.shortcuts?.toggleInvisibleOverlayGlobal ?? - defaultConfig.shortcuts?.toggleInvisibleOverlayGlobal, - ), copySubtitle: normalizeShortcut( config.shortcuts?.copySubtitle ?? defaultConfig.shortcuts?.copySubtitle, ), diff --git a/src/main/cli-runtime.ts b/src/main/cli-runtime.ts index 29c1cc9..21c2d17 100644 --- a/src/main/cli-runtime.ts +++ b/src/main/cli-runtime.ts @@ -17,9 +17,7 @@ export interface CliCommandRuntimeServiceContext { isOverlayInitialized: () => boolean; initializeOverlay: () => void; toggleVisibleOverlay: () => void; - toggleInvisibleOverlay: () => void; setVisibleOverlay: (visible: boolean) => void; - setInvisibleOverlay: (visible: boolean) => void; copyCurrentSubtitle: () => void; startPendingMultiCopy: (timeoutMs: number) => void; mineSentenceCard: () => Promise; @@ -74,9 +72,7 @@ function createCliCommandDepsFromContext( isInitialized: context.isOverlayInitialized, initialize: context.initializeOverlay, toggleVisible: context.toggleVisibleOverlay, - toggleInvisible: context.toggleInvisibleOverlay, setVisible: context.setVisibleOverlay, - setInvisible: context.setInvisibleOverlay, }, mining: { copyCurrentSubtitle: context.copyCurrentSubtitle, diff --git a/src/main/dependencies.ts b/src/main/dependencies.ts index 2ab12f3..829fb2c 100644 --- a/src/main/dependencies.ts +++ b/src/main/dependencies.ts @@ -53,11 +53,10 @@ export function createSubsyncRuntimeDeps(params: SubsyncRuntimeDepsParams): Subs } export interface MainIpcRuntimeServiceDepsParams { - getInvisibleWindow: IpcDepsRuntimeOptions['getInvisibleWindow']; getMainWindow: IpcDepsRuntimeOptions['getMainWindow']; getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility']; - getInvisibleOverlayVisibility: IpcDepsRuntimeOptions['getInvisibleOverlayVisibility']; onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed']; + onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened']; openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings']; quitApp: IpcDepsRuntimeOptions['quitApp']; toggleVisibleOverlay: IpcDepsRuntimeOptions['toggleVisibleOverlay']; @@ -65,7 +64,6 @@ export interface MainIpcRuntimeServiceDepsParams { getCurrentSubtitleRaw: IpcDepsRuntimeOptions['getCurrentSubtitleRaw']; getCurrentSubtitleAss: IpcDepsRuntimeOptions['getCurrentSubtitleAss']; focusMainWindow?: IpcDepsRuntimeOptions['focusMainWindow']; - getMpvSubtitleRenderMetrics: IpcDepsRuntimeOptions['getMpvSubtitleRenderMetrics']; getSubtitlePosition: IpcDepsRuntimeOptions['getSubtitlePosition']; getSubtitleStyle: IpcDepsRuntimeOptions['getSubtitleStyle']; saveSubtitlePosition: IpcDepsRuntimeOptions['saveSubtitlePosition']; @@ -81,7 +79,6 @@ export interface MainIpcRuntimeServiceDepsParams { setRuntimeOption: IpcDepsRuntimeOptions['setRuntimeOption']; cycleRuntimeOption: IpcDepsRuntimeOptions['cycleRuntimeOption']; reportOverlayContentBounds: IpcDepsRuntimeOptions['reportOverlayContentBounds']; - reportHoveredSubtitleToken: IpcDepsRuntimeOptions['reportHoveredSubtitleToken']; getAnilistStatus: IpcDepsRuntimeOptions['getAnilistStatus']; clearAnilistToken: IpcDepsRuntimeOptions['clearAnilistToken']; openAnilistSetup: IpcDepsRuntimeOptions['openAnilistSetup']; @@ -132,9 +129,7 @@ export interface CliCommandRuntimeServiceDepsParams { isInitialized: CliCommandDepsRuntimeOptions['overlay']['isInitialized']; initialize: CliCommandDepsRuntimeOptions['overlay']['initialize']; toggleVisible: CliCommandDepsRuntimeOptions['overlay']['toggleVisible']; - toggleInvisible: CliCommandDepsRuntimeOptions['overlay']['toggleInvisible']; setVisible: CliCommandDepsRuntimeOptions['overlay']['setVisible']; - setInvisible: CliCommandDepsRuntimeOptions['overlay']['setInvisible']; }; mining: { copyCurrentSubtitle: CliCommandDepsRuntimeOptions['mining']['copyCurrentSubtitle']; @@ -192,18 +187,16 @@ export function createMainIpcRuntimeServiceDeps( params: MainIpcRuntimeServiceDepsParams, ): IpcDepsRuntimeOptions { return { - getInvisibleWindow: params.getInvisibleWindow, getMainWindow: params.getMainWindow, getVisibleOverlayVisibility: params.getVisibleOverlayVisibility, - getInvisibleOverlayVisibility: params.getInvisibleOverlayVisibility, onOverlayModalClosed: params.onOverlayModalClosed, + onOverlayModalOpened: params.onOverlayModalOpened, openYomitanSettings: params.openYomitanSettings, quitApp: params.quitApp, toggleVisibleOverlay: params.toggleVisibleOverlay, tokenizeCurrentSubtitle: params.tokenizeCurrentSubtitle, getCurrentSubtitleRaw: params.getCurrentSubtitleRaw, getCurrentSubtitleAss: params.getCurrentSubtitleAss, - getMpvSubtitleRenderMetrics: params.getMpvSubtitleRenderMetrics, getSubtitlePosition: params.getSubtitlePosition, getSubtitleStyle: params.getSubtitleStyle, saveSubtitlePosition: params.saveSubtitlePosition, @@ -220,7 +213,6 @@ export function createMainIpcRuntimeServiceDeps( setRuntimeOption: params.setRuntimeOption, cycleRuntimeOption: params.cycleRuntimeOption, reportOverlayContentBounds: params.reportOverlayContentBounds, - reportHoveredSubtitleToken: params.reportHoveredSubtitleToken, getAnilistStatus: params.getAnilistStatus, clearAnilistToken: params.clearAnilistToken, openAnilistSetup: params.openAnilistSetup, @@ -279,9 +271,7 @@ export function createCliCommandRuntimeServiceDeps( isInitialized: params.overlay.isInitialized, initialize: params.overlay.initialize, toggleVisible: params.overlay.toggleVisible, - toggleInvisible: params.overlay.toggleInvisible, setVisible: params.overlay.setVisible, - setInvisible: params.overlay.setInvisible, }, mining: { copyCurrentSubtitle: params.mining.copyCurrentSubtitle, diff --git a/src/main/overlay-visibility-runtime.ts b/src/main/overlay-visibility-runtime.ts index 032d444..46db4cd 100644 --- a/src/main/overlay-visibility-runtime.ts +++ b/src/main/overlay-visibility-runtime.ts @@ -2,50 +2,31 @@ import type { BrowserWindow } from 'electron'; import type { BaseWindowTracker } from '../window-trackers'; import type { WindowGeometry } from '../types'; -import { - syncInvisibleOverlayMousePassthrough, - updateInvisibleOverlayVisibility, - updateVisibleOverlayVisibility, -} from '../core/services'; +import { updateVisibleOverlayVisibility } from '../core/services'; export interface OverlayVisibilityRuntimeDeps { getMainWindow: () => BrowserWindow | null; - getInvisibleWindow: () => BrowserWindow | null; getVisibleOverlayVisible: () => boolean; - getInvisibleOverlayVisible: () => boolean; getWindowTracker: () => BaseWindowTracker | null; getTrackerNotReadyWarningShown: () => boolean; setTrackerNotReadyWarningShown: (shown: boolean) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; ensureOverlayWindowLevel: (window: BrowserWindow) => void; + syncPrimaryOverlayWindowLayer: (layer: 'visible') => void; enforceOverlayLayerOrder: () => void; syncOverlayShortcuts: () => void; + isMacOSPlatform: () => boolean; + showOverlayLoadingOsd: (message: string) => void; + resolveFallbackBounds: () => WindowGeometry; } export interface OverlayVisibilityRuntimeService { updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; - syncInvisibleOverlayMousePassthrough: () => void; } export function createOverlayVisibilityRuntimeService( deps: OverlayVisibilityRuntimeDeps, ): OverlayVisibilityRuntimeService { - const hasInvisibleWindow = (): boolean => { - const invisibleWindow = deps.getInvisibleWindow(); - return Boolean(invisibleWindow && !invisibleWindow.isDestroyed()); - }; - - const setIgnoreMouseEvents = ( - ignore: boolean, - options?: Parameters[1], - ): void => { - const invisibleWindow = deps.getInvisibleWindow(); - if (!invisibleWindow || invisibleWindow.isDestroyed()) return; - invisibleWindow.setIgnoreMouseEvents(ignore, options); - }; - return { updateVisibleOverlayVisibility(): void { updateVisibleOverlayVisibility({ @@ -59,31 +40,13 @@ export function createOverlayVisibilityRuntimeService( updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry), ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), + syncPrimaryOverlayWindowLayer: (layer: 'visible') => + deps.syncPrimaryOverlayWindowLayer(layer), enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(), syncOverlayShortcuts: () => deps.syncOverlayShortcuts(), - }); - }, - - updateInvisibleOverlayVisibility(): void { - updateInvisibleOverlayVisibility({ - invisibleWindow: deps.getInvisibleWindow(), - visibleOverlayVisible: deps.getVisibleOverlayVisible(), - invisibleOverlayVisible: deps.getInvisibleOverlayVisible(), - windowTracker: deps.getWindowTracker(), - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => - deps.updateInvisibleOverlayBounds(geometry), - ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), - enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(), - syncOverlayShortcuts: () => deps.syncOverlayShortcuts(), - }); - }, - - syncInvisibleOverlayMousePassthrough(): void { - syncInvisibleOverlayMousePassthrough({ - hasInvisibleWindow, - setIgnoreMouseEvents, - visibleOverlayVisible: deps.getVisibleOverlayVisible(), - invisibleOverlayVisible: deps.getInvisibleOverlayVisible(), + isMacOSPlatform: deps.isMacOSPlatform(), + showOverlayLoadingOsd: (message: string) => deps.showOverlayLoadingOsd(message), + resolveFallbackBounds: () => deps.resolveFallbackBounds(), }); }, }; diff --git a/src/main/runtime/app-lifecycle-main-activate.test.ts b/src/main/runtime/app-lifecycle-main-activate.test.ts index 8569cc6..786078c 100644 --- a/src/main/runtime/app-lifecycle-main-activate.test.ts +++ b/src/main/runtime/app-lifecycle-main-activate.test.ts @@ -19,14 +19,12 @@ test('restore windows on activate deps builder maps all restoration callbacks', const calls: string[] = []; const deps = createBuildRestoreWindowsOnActivateMainDepsHandler({ createMainWindow: () => calls.push('main'), - createInvisibleWindow: () => calls.push('invisible'), updateVisibleOverlayVisibility: () => calls.push('visible'), - updateInvisibleOverlayVisibility: () => calls.push('invisible-visible'), + syncOverlayMpvSubtitleSuppression: () => calls.push('mpv-sync'), })(); deps.createMainWindow(); - deps.createInvisibleWindow(); deps.updateVisibleOverlayVisibility(); - deps.updateInvisibleOverlayVisibility(); - assert.deepEqual(calls, ['main', 'invisible', 'visible', 'invisible-visible']); + deps.syncOverlayMpvSubtitleSuppression(); + assert.deepEqual(calls, ['main', 'visible', 'mpv-sync']); }); diff --git a/src/main/runtime/app-lifecycle-main-activate.ts b/src/main/runtime/app-lifecycle-main-activate.ts index 3fde767..38168a0 100644 --- a/src/main/runtime/app-lifecycle-main-activate.ts +++ b/src/main/runtime/app-lifecycle-main-activate.ts @@ -10,14 +10,12 @@ export function createBuildShouldRestoreWindowsOnActivateMainDepsHandler(deps: { export function createBuildRestoreWindowsOnActivateMainDepsHandler(deps: { createMainWindow: () => void; - createInvisibleWindow: () => void; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; + syncOverlayMpvSubtitleSuppression: () => void; }) { return () => ({ createMainWindow: () => deps.createMainWindow(), - createInvisibleWindow: () => deps.createInvisibleWindow(), updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), - updateInvisibleOverlayVisibility: () => deps.updateInvisibleOverlayVisibility(), + syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(), }); } diff --git a/src/main/runtime/app-runtime-main-deps.test.ts b/src/main/runtime/app-runtime-main-deps.test.ts index c0767a5..37252d5 100644 --- a/src/main/runtime/app-runtime-main-deps.test.ts +++ b/src/main/runtime/app-runtime-main-deps.test.ts @@ -50,26 +50,18 @@ test('initialize overlay runtime main deps map build options and callbacks', () isOverlayRuntimeInitialized: () => false, initializeOverlayRuntimeCore: (value) => { calls.push(`core:${JSON.stringify(value)}`); - return { invisibleOverlayVisible: true }; }, buildOptions: () => options, - setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`), setOverlayRuntimeInitialized: (initialized) => calls.push(`set-initialized:${initialized}`), startBackgroundWarmups: () => calls.push('warmups'), })(); assert.equal(deps.isOverlayRuntimeInitialized(), false); assert.equal(deps.buildOptions(), options); - assert.deepEqual(deps.initializeOverlayRuntimeCore(options), { invisibleOverlayVisible: true }); - deps.setInvisibleOverlayVisible(true); + assert.equal(deps.initializeOverlayRuntimeCore(options), undefined); deps.setOverlayRuntimeInitialized(true); deps.startBackgroundWarmups(); - assert.deepEqual(calls, [ - 'core:{"id":"opts"}', - 'set-invisible:true', - 'set-initialized:true', - 'warmups', - ]); + assert.deepEqual(calls, ['core:{"id":"opts"}', 'set-initialized:true', 'warmups']); }); test('open yomitan settings main deps map async open callbacks', async () => { diff --git a/src/main/runtime/app-runtime-main-deps.ts b/src/main/runtime/app-runtime-main-deps.ts index da27598..278de88 100644 --- a/src/main/runtime/app-runtime-main-deps.ts +++ b/src/main/runtime/app-runtime-main-deps.ts @@ -45,9 +45,8 @@ export function createBuildDestroyTrayMainDepsHandler(deps: { export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps: { isOverlayRuntimeInitialized: () => boolean; - initializeOverlayRuntimeCore: (options: TOptions) => { invisibleOverlayVisible: boolean }; + initializeOverlayRuntimeCore: (options: TOptions) => void; buildOptions: () => TOptions; - setInvisibleOverlayVisible: (visible: boolean) => void; setOverlayRuntimeInitialized: (initialized: boolean) => void; startBackgroundWarmups: () => void; }) { @@ -55,7 +54,6 @@ export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler deps.isOverlayRuntimeInitialized(), initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options), buildOptions: () => deps.buildOptions(), - setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), setOverlayRuntimeInitialized: (initialized: boolean) => deps.setOverlayRuntimeInitialized(initialized), startBackgroundWarmups: () => deps.startBackgroundWarmups(), diff --git a/src/main/runtime/cli-command-context-deps.test.ts b/src/main/runtime/cli-command-context-deps.test.ts index 53540b6..3bb631b 100644 --- a/src/main/runtime/cli-command-context-deps.test.ts +++ b/src/main/runtime/cli-command-context-deps.test.ts @@ -18,9 +18,7 @@ test('build cli command context deps maps handlers and values', () => { isOverlayInitialized: () => true, initializeOverlay: () => calls.push('init'), toggleVisibleOverlay: () => calls.push('toggle-visible'), - toggleInvisibleOverlay: () => calls.push('toggle-invisible'), setVisibleOverlay: (visible) => calls.push(`set-visible:${visible}`), - setInvisibleOverlay: (visible) => calls.push(`set-invisible:${visible}`), copyCurrentSubtitle: () => calls.push('copy'), startPendingMultiCopy: (ms) => calls.push(`multi:${ms}`), mineSentenceCard: async () => { diff --git a/src/main/runtime/cli-command-context-deps.ts b/src/main/runtime/cli-command-context-deps.ts index f268210..ebd31f4 100644 --- a/src/main/runtime/cli-command-context-deps.ts +++ b/src/main/runtime/cli-command-context-deps.ts @@ -15,9 +15,7 @@ export function createBuildCliCommandContextDepsHandler(deps: { isOverlayInitialized: () => boolean; initializeOverlay: () => void; toggleVisibleOverlay: () => void; - toggleInvisibleOverlay: () => void; setVisibleOverlay: (visible: boolean) => void; - setInvisibleOverlay: (visible: boolean) => void; copyCurrentSubtitle: () => void; startPendingMultiCopy: (timeoutMs: number) => void; mineSentenceCard: () => Promise; @@ -60,9 +58,7 @@ export function createBuildCliCommandContextDepsHandler(deps: { isOverlayInitialized: deps.isOverlayInitialized, initializeOverlay: deps.initializeOverlay, toggleVisibleOverlay: deps.toggleVisibleOverlay, - toggleInvisibleOverlay: deps.toggleInvisibleOverlay, setVisibleOverlay: deps.setVisibleOverlay, - setInvisibleOverlay: deps.setInvisibleOverlay, copyCurrentSubtitle: deps.copyCurrentSubtitle, startPendingMultiCopy: deps.startPendingMultiCopy, mineSentenceCard: deps.mineSentenceCard, diff --git a/src/main/runtime/cli-command-context-factory.test.ts b/src/main/runtime/cli-command-context-factory.test.ts index 584e6c4..43ed49c 100644 --- a/src/main/runtime/cli-command-context-factory.test.ts +++ b/src/main/runtime/cli-command-context-factory.test.ts @@ -20,9 +20,7 @@ test('cli command context factory composes main deps and context handlers', () = showMpvOsd: (text) => calls.push(`osd:${text}`), initializeOverlayRuntime: () => calls.push('init-overlay'), toggleVisibleOverlay: () => calls.push('toggle-visible'), - toggleInvisibleOverlay: () => calls.push('toggle-invisible'), setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`), - setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`), copyCurrentSubtitle: () => calls.push('copy-sub'), startPendingMultiCopy: (timeoutMs) => calls.push(`multi:${timeoutMs}`), mineSentenceCard: async () => {}, @@ -73,16 +71,8 @@ test('cli command context factory composes main deps and context handlers', () = context.setSocketPath('/tmp/new.sock'); context.showOsd('hello'); context.setVisibleOverlay(true); - context.setInvisibleOverlay(false); context.toggleVisibleOverlay(); - context.toggleInvisibleOverlay(); assert.equal(appState.mpvSocketPath, '/tmp/new.sock'); - assert.deepEqual(calls, [ - 'osd:hello', - 'set-visible:true', - 'set-invisible:false', - 'toggle-visible', - 'toggle-invisible', - ]); + assert.deepEqual(calls, ['osd:hello', 'set-visible:true', 'toggle-visible']); }); diff --git a/src/main/runtime/cli-command-context-main-deps.test.ts b/src/main/runtime/cli-command-context-main-deps.test.ts index 7a06eb3..24109bd 100644 --- a/src/main/runtime/cli-command-context-main-deps.test.ts +++ b/src/main/runtime/cli-command-context-main-deps.test.ts @@ -23,9 +23,7 @@ test('cli command context main deps builder maps state and callbacks', async () initializeOverlayRuntime: () => calls.push('init-overlay'), toggleVisibleOverlay: () => calls.push('toggle-visible'), - toggleInvisibleOverlay: () => calls.push('toggle-invisible'), setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`), - setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`), copyCurrentSubtitle: () => calls.push('copy-sub'), startPendingMultiCopy: (timeoutMs) => calls.push(`multi:${timeoutMs}`), @@ -103,16 +101,9 @@ test('cli command context main deps builder maps state and callbacks', async () deps.showOsd('hello'); deps.initializeOverlay(); deps.setVisibleOverlay(true); - deps.setInvisibleOverlay(false); deps.printHelp(); - assert.deepEqual(calls, [ - 'osd:hello', - 'init-overlay', - 'set-visible:true', - 'set-invisible:false', - 'help', - ]); + assert.deepEqual(calls, ['osd:hello', 'init-overlay', 'set-visible:true', 'help']); const retry = await deps.retryAnilistQueueNow(); assert.deepEqual(retry, { ok: true, message: 'ok' }); diff --git a/src/main/runtime/cli-command-context-main-deps.ts b/src/main/runtime/cli-command-context-main-deps.ts index 5200fd8..831c89d 100644 --- a/src/main/runtime/cli-command-context-main-deps.ts +++ b/src/main/runtime/cli-command-context-main-deps.ts @@ -18,9 +18,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: { initializeOverlayRuntime: () => void; toggleVisibleOverlay: () => void; - toggleInvisibleOverlay: () => void; setVisibleOverlayVisible: (visible: boolean) => void; - setInvisibleOverlayVisible: (visible: boolean) => void; copyCurrentSubtitle: () => void; startPendingMultiCopy: (timeoutMs: number) => void; @@ -70,9 +68,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: { isOverlayInitialized: () => deps.appState.overlayRuntimeInitialized, initializeOverlay: () => deps.initializeOverlayRuntime(), toggleVisibleOverlay: () => deps.toggleVisibleOverlay(), - toggleInvisibleOverlay: () => deps.toggleInvisibleOverlay(), setVisibleOverlay: (visible: boolean) => deps.setVisibleOverlayVisible(visible), - setInvisibleOverlay: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), copyCurrentSubtitle: () => deps.copyCurrentSubtitle(), startPendingMultiCopy: (timeoutMs: number) => deps.startPendingMultiCopy(timeoutMs), mineSentenceCard: () => deps.mineSentenceCard(), diff --git a/src/main/runtime/cli-command-context.test.ts b/src/main/runtime/cli-command-context.test.ts index 6d73755..1417433 100644 --- a/src/main/runtime/cli-command-context.test.ts +++ b/src/main/runtime/cli-command-context.test.ts @@ -24,9 +24,7 @@ function createDeps() { isOverlayInitialized: () => true, initializeOverlay: () => {}, toggleVisibleOverlay: () => {}, - toggleInvisibleOverlay: () => {}, setVisibleOverlay: () => {}, - setInvisibleOverlay: () => {}, copyCurrentSubtitle: () => {}, startPendingMultiCopy: () => {}, mineSentenceCard: async () => {}, diff --git a/src/main/runtime/cli-command-context.ts b/src/main/runtime/cli-command-context.ts index a644961..d2aa185 100644 --- a/src/main/runtime/cli-command-context.ts +++ b/src/main/runtime/cli-command-context.ts @@ -20,9 +20,7 @@ export type CliCommandContextFactoryDeps = { isOverlayInitialized: () => boolean; initializeOverlay: () => void; toggleVisibleOverlay: () => void; - toggleInvisibleOverlay: () => void; setVisibleOverlay: (visible: boolean) => void; - setInvisibleOverlay: (visible: boolean) => void; copyCurrentSubtitle: () => void; startPendingMultiCopy: (timeoutMs: number) => void; mineSentenceCard: () => Promise; @@ -72,9 +70,7 @@ export function createCliCommandContext( isOverlayInitialized: deps.isOverlayInitialized, initializeOverlay: deps.initializeOverlay, toggleVisibleOverlay: deps.toggleVisibleOverlay, - toggleInvisibleOverlay: deps.toggleInvisibleOverlay, setVisibleOverlay: deps.setVisibleOverlay, - setInvisibleOverlay: deps.setInvisibleOverlay, copyCurrentSubtitle: deps.copyCurrentSubtitle, startPendingMultiCopy: deps.startPendingMultiCopy, mineSentenceCard: deps.mineSentenceCard, diff --git a/src/main/runtime/composers/ipc-runtime-composer.test.ts b/src/main/runtime/composers/ipc-runtime-composer.test.ts index 51596dc..b862b89 100644 --- a/src/main/runtime/composers/ipc-runtime-composer.test.ts +++ b/src/main/runtime/composers/ipc-runtime-composer.test.ts @@ -32,10 +32,8 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b showMpvOsd: () => {}, }, mainDeps: { - getInvisibleWindow: () => null, getMainWindow: () => null, getVisibleOverlayVisibility: () => false, - getInvisibleOverlayVisibility: () => false, focusMainWindow: () => {}, onOverlayModalClosed: () => {}, openYomitanSettings: () => {}, @@ -44,7 +42,6 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b tokenizeCurrentSubtitle: async () => null, getCurrentSubtitleRaw: () => '', getCurrentSubtitleAss: () => '', - getMpvSubtitleRenderMetrics: () => ({}) as never, getSubtitlePosition: () => ({}) as never, getSubtitleStyle: () => ({}) as never, saveSubtitlePosition: () => {}, @@ -56,7 +53,6 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b getAnkiConnectStatus: () => false, getRuntimeOptions: () => [], reportOverlayContentBounds: () => {}, - reportHoveredSubtitleToken: () => {}, getAnilistStatus: () => ({}) as never, clearAnilistToken: () => {}, openAnilistSetup: () => {}, diff --git a/src/main/runtime/composers/mpv-runtime-composer.test.ts b/src/main/runtime/composers/mpv-runtime-composer.test.ts index ca4654f..440766c 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.test.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.test.ts @@ -68,12 +68,14 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject scheduleQuitCheck: () => {}, quitApp: () => {}, reportJellyfinRemoteStopped: () => {}, + syncOverlayMpvSubtitleSuppression: () => {}, maybeRunAnilistPostWatchUpdate: async () => {}, logSubtitleTimingError: () => {}, broadcastToOverlayWindows: () => {}, onSubtitleChange: () => {}, refreshDiscordPresence: () => {}, updateCurrentMediaPath: () => {}, + restoreMpvSubVisibilityForInvisibleOverlay: () => {}, getCurrentAnilistMediaKey: () => null, resetAnilistMediaTracking: () => {}, maybeProbeAnilistDuration: () => {}, diff --git a/src/main/runtime/composers/shortcuts-runtime-composer.test.ts b/src/main/runtime/composers/shortcuts-runtime-composer.test.ts index f498a14..a93b547 100644 --- a/src/main/runtime/composers/shortcuts-runtime-composer.test.ts +++ b/src/main/runtime/composers/shortcuts-runtime-composer.test.ts @@ -14,7 +14,6 @@ test('composeShortcutRuntimes returns callable shortcut runtime handlers', () => getConfiguredShortcuts: () => ({}) as never, registerGlobalShortcutsCore: () => {}, toggleVisibleOverlay: () => {}, - toggleInvisibleOverlay: () => {}, openYomitanSettings: () => {}, isDev: false, getMainWindow: () => null, diff --git a/src/main/runtime/config-derived.ts b/src/main/runtime/config-derived.ts index 3c90fc0..ac97855 100644 --- a/src/main/runtime/config-derived.ts +++ b/src/main/runtime/config-derived.ts @@ -1,7 +1,6 @@ import type { RuntimeOptionsManager } from '../../runtime-options'; import type { JimakuApiResponse, JimakuLanguagePreference, ResolvedConfig } from '../../types'; import { - getInitialInvisibleOverlayVisibility as getInitialInvisibleOverlayVisibilityCore, getJimakuLanguagePreference as getJimakuLanguagePreferenceCore, getJimakuMaxEntryResults as getJimakuMaxEntryResultsCore, isAutoUpdateEnabledRuntime as isAutoUpdateEnabledRuntimeCore, @@ -14,14 +13,12 @@ import { export type ConfigDerivedRuntimeDeps = { getResolvedConfig: () => ResolvedConfig; getRuntimeOptionsManager: () => RuntimeOptionsManager | null; - platform: NodeJS.Platform; defaultJimakuLanguagePreference: JimakuLanguagePreference; defaultJimakuMaxEntryResults: number; defaultJimakuApiBaseUrl: string; }; export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): { - getInitialInvisibleOverlayVisibility: () => boolean; shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean; shouldBindVisibleOverlayToMpvSubVisibility: () => boolean; isAutoUpdateEnabledRuntime: () => boolean; @@ -34,8 +31,6 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): { ) => Promise>; } { return { - getInitialInvisibleOverlayVisibility: () => - getInitialInvisibleOverlayVisibilityCore(deps.getResolvedConfig(), deps.platform), shouldAutoInitializeOverlayRuntimeFromConfig: () => shouldAutoInitializeOverlayRuntimeFromConfigCore(deps.getResolvedConfig()), shouldBindVisibleOverlayToMpvSubVisibility: () => diff --git a/src/main/runtime/field-grouping-overlay-main-deps.test.ts b/src/main/runtime/field-grouping-overlay-main-deps.test.ts index 4fac2e6..662dea3 100644 --- a/src/main/runtime/field-grouping-overlay-main-deps.test.ts +++ b/src/main/runtime/field-grouping-overlay-main-deps.test.ts @@ -15,9 +15,7 @@ test('field grouping overlay main deps builder maps window visibility and resolv }, }), getVisibleOverlayVisible: () => true, - getInvisibleOverlayVisible: () => false, setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`), - setInvisibleOverlayVisible: (visible) => calls.push(`invisible:${visible}`), getResolver: () => resolver, setResolver: (nextResolver) => { calls.push(`set-resolver:${nextResolver ? 'set' : 'null'}`); @@ -31,17 +29,10 @@ test('field grouping overlay main deps builder maps window visibility and resolv assert.equal(deps.getMainWindow()?.isDestroyed(), false); assert.equal(deps.getVisibleOverlayVisible(), true); - assert.equal(deps.getInvisibleOverlayVisible(), false); assert.equal(deps.getResolver(), resolver); assert.equal(deps.getRestoreVisibleOverlayOnModalClose(), modalSet); deps.setVisibleOverlayVisible(true); - deps.setInvisibleOverlayVisible(false); deps.setResolver(null); assert.equal(deps.sendToVisibleOverlay('kiku:open', 1), true); - assert.deepEqual(calls, [ - 'visible:true', - 'invisible:false', - 'set-resolver:null', - 'send:kiku:open:1', - ]); + assert.deepEqual(calls, ['visible:true', 'set-resolver:null', 'send:kiku:open:1']); }); diff --git a/src/main/runtime/field-grouping-overlay-main-deps.ts b/src/main/runtime/field-grouping-overlay-main-deps.ts index 9f3de0a..1dbf8a3 100644 --- a/src/main/runtime/field-grouping-overlay-main-deps.ts +++ b/src/main/runtime/field-grouping-overlay-main-deps.ts @@ -24,9 +24,7 @@ export function createBuildFieldGroupingOverlayMainDepsHandler => ({ getMainWindow: () => deps.getMainWindow(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), - getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible), - setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), getResolver: () => deps.getResolver(), setResolver: (resolver) => deps.setResolver(resolver), getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(), diff --git a/src/main/runtime/global-shortcuts-main-deps.test.ts b/src/main/runtime/global-shortcuts-main-deps.test.ts index 4a9cb00..e4f086c 100644 --- a/src/main/runtime/global-shortcuts-main-deps.test.ts +++ b/src/main/runtime/global-shortcuts-main-deps.test.ts @@ -28,7 +28,6 @@ test('register global shortcuts main deps map callbacks and flags', () => { getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never), registerGlobalShortcutsCore: () => calls.push('register'), toggleVisibleOverlay: () => calls.push('toggle-visible'), - toggleInvisibleOverlay: () => calls.push('toggle-invisible'), openYomitanSettings: () => calls.push('open-yomitan'), isDev: true, getMainWindow: () => mainWindow as never, @@ -38,17 +37,15 @@ test('register global shortcuts main deps map callbacks and flags', () => { deps.registerGlobalShortcutsCore({ shortcuts: deps.getConfiguredShortcuts(), onToggleVisibleOverlay: () => undefined, - onToggleInvisibleOverlay: () => undefined, onOpenYomitanSettings: () => undefined, isDev: deps.isDev, getMainWindow: deps.getMainWindow, }); deps.onToggleVisibleOverlay(); - deps.onToggleInvisibleOverlay(); deps.onOpenYomitanSettings(); assert.equal(deps.isDev, true); assert.deepEqual(deps.getMainWindow(), mainWindow); - assert.deepEqual(calls, ['register', 'toggle-visible', 'toggle-invisible', 'open-yomitan']); + assert.deepEqual(calls, ['register', 'toggle-visible', 'open-yomitan']); }); test('refresh global shortcuts main deps map passthrough handlers', () => { diff --git a/src/main/runtime/global-shortcuts-main-deps.ts b/src/main/runtime/global-shortcuts-main-deps.ts index 779950f..5909d18 100644 --- a/src/main/runtime/global-shortcuts-main-deps.ts +++ b/src/main/runtime/global-shortcuts-main-deps.ts @@ -19,7 +19,6 @@ export function createBuildRegisterGlobalShortcutsMainDepsHandler(deps: { getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts']; registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void; toggleVisibleOverlay: () => void; - toggleInvisibleOverlay: () => void; openYomitanSettings: () => void; isDev: boolean; getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow']; @@ -29,7 +28,6 @@ export function createBuildRegisterGlobalShortcutsMainDepsHandler(deps: { registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => deps.registerGlobalShortcutsCore(options), onToggleVisibleOverlay: () => deps.toggleVisibleOverlay(), - onToggleInvisibleOverlay: () => deps.toggleInvisibleOverlay(), onOpenYomitanSettings: () => deps.openYomitanSettings(), isDev: deps.isDev, getMainWindow: deps.getMainWindow, diff --git a/src/main/runtime/global-shortcuts-runtime-handlers.test.ts b/src/main/runtime/global-shortcuts-runtime-handlers.test.ts index 2665177..963d84f 100644 --- a/src/main/runtime/global-shortcuts-runtime-handlers.test.ts +++ b/src/main/runtime/global-shortcuts-runtime-handlers.test.ts @@ -6,7 +6,6 @@ import { createGlobalShortcutsRuntimeHandlers } from './global-shortcuts-runtime function createShortcuts(): ConfiguredShortcuts { return { toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O', - toggleInvisibleOverlayGlobal: 'CommandOrControl+Shift+I', copySubtitle: 's', copySubtitleMultiple: 'CommandOrControl+s', updateLastCardFromClipboard: 'c', @@ -38,7 +37,6 @@ test('global shortcuts runtime handlers compose get/register/refresh flow', () = assert.equal(options.shortcuts, shortcuts); }, toggleVisibleOverlay: () => calls.push('toggle-visible'), - toggleInvisibleOverlay: () => calls.push('toggle-invisible'), openYomitanSettings: () => calls.push('open-yomitan'), isDev: false, getMainWindow: () => null, diff --git a/src/main/runtime/global-shortcuts.test.ts b/src/main/runtime/global-shortcuts.test.ts index 54cee83..a2fe05c 100644 --- a/src/main/runtime/global-shortcuts.test.ts +++ b/src/main/runtime/global-shortcuts.test.ts @@ -10,7 +10,6 @@ import type { ConfiguredShortcuts } from '../../core/utils/shortcut-config'; function createShortcuts(): ConfiguredShortcuts { return { toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O', - toggleInvisibleOverlayGlobal: 'CommandOrControl+Shift+I', copySubtitle: 's', copySubtitleMultiple: 'CommandOrControl+s', updateLastCardFromClipboard: 'c', @@ -58,18 +57,16 @@ test('register global shortcuts handler passes through callbacks and shortcuts', assert.equal(options.isDev, true); assert.equal(options.getMainWindow(), mainWindow); options.onToggleVisibleOverlay(); - options.onToggleInvisibleOverlay(); options.onOpenYomitanSettings(); }, onToggleVisibleOverlay: () => calls.push('toggle-visible'), - onToggleInvisibleOverlay: () => calls.push('toggle-invisible'), onOpenYomitanSettings: () => calls.push('open-yomitan'), isDev: true, getMainWindow: () => mainWindow, }); registerGlobalShortcuts(); - assert.deepEqual(calls, ['register', 'toggle-visible', 'toggle-invisible', 'open-yomitan']); + assert.deepEqual(calls, ['register', 'toggle-visible', 'open-yomitan']); }); test('refresh global and overlay shortcuts unregisters then re-registers', () => { diff --git a/src/main/runtime/global-shortcuts.ts b/src/main/runtime/global-shortcuts.ts index 4268c76..825b062 100644 --- a/src/main/runtime/global-shortcuts.ts +++ b/src/main/runtime/global-shortcuts.ts @@ -18,7 +18,6 @@ export function createRegisterGlobalShortcutsHandler(deps: { getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts']; registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void; onToggleVisibleOverlay: () => void; - onToggleInvisibleOverlay: () => void; onOpenYomitanSettings: () => void; isDev: boolean; getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow']; @@ -27,7 +26,6 @@ export function createRegisterGlobalShortcutsHandler(deps: { deps.registerGlobalShortcutsCore({ shortcuts: deps.getConfiguredShortcuts(), onToggleVisibleOverlay: deps.onToggleVisibleOverlay, - onToggleInvisibleOverlay: deps.onToggleInvisibleOverlay, onOpenYomitanSettings: deps.onOpenYomitanSettings, isDev: deps.isDev, getMainWindow: deps.getMainWindow, diff --git a/src/main/runtime/jellyfin-command-dispatch.test.ts b/src/main/runtime/jellyfin-command-dispatch.test.ts index 5f0b39b..7c88923 100644 --- a/src/main/runtime/jellyfin-command-dispatch.test.ts +++ b/src/main/runtime/jellyfin-command-dispatch.test.ts @@ -24,9 +24,6 @@ function createArgs(overrides: Partial = {}): CliArgs { toggleOverlay: false, hideOverlay: false, showOverlay: false, - toggleInvisibleOverlay: false, - hideInvisibleOverlay: false, - showInvisibleOverlay: false, copyCurrentSubtitle: false, multiCopy: false, mineSentence: false, diff --git a/src/main/runtime/mpv-client-event-bindings.test.ts b/src/main/runtime/mpv-client-event-bindings.test.ts index 16655ab..5f4bae7 100644 --- a/src/main/runtime/mpv-client-event-bindings.test.ts +++ b/src/main/runtime/mpv-client-event-bindings.test.ts @@ -11,6 +11,7 @@ test('mpv connection handler reports stop and quits when disconnect guard passes const handler = createHandleMpvConnectionChangeHandler({ reportJellyfinRemoteStopped: () => calls.push('report-stop'), refreshDiscordPresence: () => calls.push('presence-refresh'), + syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'), hasInitialJellyfinPlayArg: () => true, isOverlayRuntimeInitialized: () => false, isQuitOnDisconnectArmed: () => true, @@ -26,6 +27,27 @@ test('mpv connection handler reports stop and quits when disconnect guard passes assert.deepEqual(calls, ['presence-refresh', 'report-stop', 'schedule', 'quit']); }); +test('mpv connection handler syncs overlay subtitle suppression on connect', () => { + const calls: string[] = []; + const handler = createHandleMpvConnectionChangeHandler({ + reportJellyfinRemoteStopped: () => calls.push('report-stop'), + refreshDiscordPresence: () => calls.push('presence-refresh'), + syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'), + hasInitialJellyfinPlayArg: () => true, + isOverlayRuntimeInitialized: () => false, + isQuitOnDisconnectArmed: () => true, + scheduleQuitCheck: () => { + calls.push('schedule'); + }, + isMpvConnected: () => false, + quitApp: () => calls.push('quit'), + }); + + handler({ connected: true }); + + assert.deepEqual(calls, ['presence-refresh', 'sync-overlay-mpv-sub']); +}); + test('mpv subtitle timing handler ignores blank subtitle lines', () => { const calls: string[] = []; const handler = createHandleMpvSubtitleTimingHandler({ diff --git a/src/main/runtime/mpv-client-event-bindings.ts b/src/main/runtime/mpv-client-event-bindings.ts index 98d016b..eefe3f8 100644 --- a/src/main/runtime/mpv-client-event-bindings.ts +++ b/src/main/runtime/mpv-client-event-bindings.ts @@ -18,6 +18,7 @@ type MpvEventClient = { export function createHandleMpvConnectionChangeHandler(deps: { reportJellyfinRemoteStopped: () => void; refreshDiscordPresence: () => void; + syncOverlayMpvSubtitleSuppression: () => void; hasInitialJellyfinPlayArg: () => boolean; isOverlayRuntimeInitialized: () => boolean; isQuitOnDisconnectArmed: () => boolean; @@ -27,7 +28,10 @@ export function createHandleMpvConnectionChangeHandler(deps: { }) { return ({ connected }: { connected: boolean }): void => { deps.refreshDiscordPresence(); - if (connected) return; + if (connected) { + deps.syncOverlayMpvSubtitleSuppression(); + return; + } deps.reportJellyfinRemoteStopped(); if (!deps.hasInitialJellyfinPlayArg()) return; if (deps.isOverlayRuntimeInitialized()) return; diff --git a/src/main/runtime/mpv-hover-highlight.test.ts b/src/main/runtime/mpv-hover-highlight.test.ts deleted file mode 100644 index 45a1fa3..0000000 --- a/src/main/runtime/mpv-hover-highlight.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { PartOfSpeech, type SubtitleData } from '../../types'; -import { - HOVER_TOKEN_MESSAGE, - HOVER_SCRIPT_NAME, - buildHoveredTokenMessageCommand, - buildHoveredTokenPayload, - createApplyHoveredTokenOverlayHandler, -} from './mpv-hover-highlight'; - -const SUBTITLE: SubtitleData = { - text: '昨日は雨だった。', - tokens: [ - { - surface: '昨日', - reading: 'きのう', - headword: '昨日', - startPos: 0, - endPos: 2, - partOfSpeech: PartOfSpeech.noun, - isMerged: false, - isKnown: false, - isNPlusOneTarget: false, - }, - { - surface: 'は', - reading: 'は', - headword: 'は', - startPos: 2, - endPos: 3, - partOfSpeech: PartOfSpeech.particle, - isMerged: false, - isKnown: true, - isNPlusOneTarget: false, - }, - { - surface: '雨', - reading: 'あめ', - headword: '雨', - startPos: 3, - endPos: 4, - partOfSpeech: PartOfSpeech.noun, - isMerged: false, - isKnown: false, - isNPlusOneTarget: true, - }, - { - surface: 'だった。', - reading: 'だった。', - headword: 'だ', - startPos: 4, - endPos: 8, - partOfSpeech: PartOfSpeech.other, - isMerged: false, - isKnown: false, - isNPlusOneTarget: false, - }, - ], -}; - -test('buildHoveredTokenPayload normalizes metadata and strips empty tokens', () => { - const payload = buildHoveredTokenPayload({ - subtitle: SUBTITLE, - hoveredTokenIndex: 2, - revision: 5, - }); - - assert.equal(payload.revision, 5); - assert.equal(payload.subtitle, '昨日は雨だった。'); - assert.equal(payload.hoveredTokenIndex, 2); - assert.equal(payload.tokens.length, 4); - assert.equal(payload.tokens[0]?.text, '昨日'); - assert.equal(payload.tokens[0]?.index, 0); - assert.equal(payload.tokens[1]?.index, 1); - assert.equal(payload.colors.hover, 'C6A0F6'); -}); - -test('buildHoveredTokenPayload normalizes hover color override', () => { - const payload = buildHoveredTokenPayload({ - subtitle: SUBTITLE, - hoveredTokenIndex: 1, - revision: 7, - hoverColor: '#c6a0f6', - }); - - assert.equal(payload.colors.hover, 'C6A0F6'); -}); - -test('buildHoveredTokenMessageCommand sends script-message-to subminer payload', () => { - const payload = buildHoveredTokenPayload({ - subtitle: SUBTITLE, - hoveredTokenIndex: 0, - revision: 1, - }); - - const command = buildHoveredTokenMessageCommand(payload); - - assert.equal(command[0], 'script-message-to'); - assert.equal(command[1], HOVER_SCRIPT_NAME); - assert.equal(command[2], HOVER_TOKEN_MESSAGE); - - const raw = command[3] as string; - const parsed = JSON.parse(raw); - assert.equal(parsed.revision, 1); - assert.equal(parsed.hoveredTokenIndex, 0); - assert.equal(parsed.subtitle, '昨日は雨だった。'); - assert.equal(parsed.tokens.length, 4); -}); - -test('createApplyHoveredTokenOverlayHandler sends clear payload when hovered token is missing', () => { - const commands: Array<(string | number)[]> = []; - const apply = createApplyHoveredTokenOverlayHandler({ - getMpvClient: () => ({ - connected: true, - send: ({ command }: { command: (string | number)[] }) => { - commands.push(command); - return true; - }, - }), - getCurrentSubtitleData: () => SUBTITLE, - getHoveredTokenIndex: () => null, - getHoveredSubtitleRevision: () => 3, - getHoverTokenColor: () => null, - }); - - apply(); - - const parsed = JSON.parse(commands[0]?.[3] as string); - assert.equal(parsed.hoveredTokenIndex, null); - assert.equal(parsed.subtitle, null); - assert.equal(parsed.tokens.length, 0); -}); - -test('createApplyHoveredTokenOverlayHandler sends highlight payload when hover is active', () => { - const commands: Array<(string | number)[]> = []; - const apply = createApplyHoveredTokenOverlayHandler({ - getMpvClient: () => ({ - connected: true, - send: ({ command }: { command: (string | number)[] }) => { - commands.push(command); - return true; - }, - }), - getCurrentSubtitleData: () => SUBTITLE, - getHoveredTokenIndex: () => 0, - getHoveredSubtitleRevision: () => 3, - getHoverTokenColor: () => '#c6a0f6', - }); - - apply(); - - const parsed = JSON.parse(commands[0]?.[3] as string); - assert.equal(parsed.hoveredTokenIndex, 0); - assert.equal(parsed.subtitle, '昨日は雨だった。'); - assert.equal(parsed.tokens.length, 4); - assert.equal(parsed.colors.hover, 'C6A0F6'); - assert.equal(commands[0]?.[0], 'script-message-to'); - assert.equal(commands[0]?.[1], HOVER_SCRIPT_NAME); -}); diff --git a/src/main/runtime/mpv-hover-highlight.ts b/src/main/runtime/mpv-hover-highlight.ts deleted file mode 100644 index 1932bfa..0000000 --- a/src/main/runtime/mpv-hover-highlight.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { SubtitleData } from '../../types'; - -export const HOVER_SCRIPT_NAME = 'subminer'; -export const HOVER_TOKEN_MESSAGE = 'subminer-hover-token'; - -const DEFAULT_HOVER_TOKEN_COLOR = 'C6A0F6'; -const DEFAULT_TOKEN_COLOR = 'FFFFFF'; - -export type HoverPayloadToken = { - text: string; - index: number; - startPos: number | null; - endPos: number | null; -}; - -export type HoverTokenPayload = { - revision: number; - subtitle: string | null; - hoveredTokenIndex: number | null; - tokens: HoverPayloadToken[]; - colors: { - base: string; - hover: string; - }; -}; - -type HoverTokenInput = { - subtitle: SubtitleData | null; - hoveredTokenIndex: number | null; - revision: number; - hoverColor?: string | null; -}; - -function normalizeHexColor(color: string | null | undefined, fallback: string): string { - if (typeof color !== 'string') { - return fallback; - } - const normalized = color.trim().replace(/^#/, '').toUpperCase(); - return /^[0-9A-F]{6}$/.test(normalized) ? normalized : fallback; -} - -function sanitizeSubtitleText(text: string): string { - return text - .replace(/\\N/g, '\n') - .replace(/\\n/g, '\n') - .replace(/\{[^}]*\}/g, '') - .trim(); -} - -function sanitizeTokenSurface(surface: unknown): string { - return typeof surface === 'string' ? surface : ''; -} - -function hasHoveredToken(subtitle: SubtitleData | null, hoveredTokenIndex: number | null): boolean { - if (!subtitle || hoveredTokenIndex === null || hoveredTokenIndex < 0) { - return false; - } - - return subtitle.tokens?.some((token, index) => index === hoveredTokenIndex) ?? false; -} - -export function buildHoveredTokenPayload(input: HoverTokenInput): HoverTokenPayload { - const { subtitle, hoveredTokenIndex, revision, hoverColor } = input; - - const tokens: HoverPayloadToken[] = []; - - if (subtitle?.tokens && subtitle.tokens.length > 0) { - for (let tokenIndex = 0; tokenIndex < subtitle.tokens.length; tokenIndex += 1) { - const token = subtitle.tokens[tokenIndex]; - if (!token) { - continue; - } - const surface = sanitizeTokenSurface(token?.surface); - if (!surface || surface.trim().length === 0) { - continue; - } - - tokens.push({ - text: surface, - index: tokenIndex, - startPos: Number.isFinite(token.startPos) ? token.startPos : null, - endPos: Number.isFinite(token.endPos) ? token.endPos : null, - }); - } - } - - return { - revision, - subtitle: subtitle ? sanitizeSubtitleText(subtitle.text) : null, - hoveredTokenIndex: - hoveredTokenIndex !== null && hoveredTokenIndex >= 0 ? hoveredTokenIndex : null, - tokens, - colors: { - base: DEFAULT_TOKEN_COLOR, - hover: normalizeHexColor(hoverColor, DEFAULT_HOVER_TOKEN_COLOR), - }, - }; -} - -export function buildHoveredTokenMessageCommand(payload: HoverTokenPayload): (string | number)[] { - return [ - 'script-message-to', - HOVER_SCRIPT_NAME, - HOVER_TOKEN_MESSAGE, - JSON.stringify(payload), - ]; -} - -export function createApplyHoveredTokenOverlayHandler(deps: { - getMpvClient: () => { - connected: boolean; - send: (payload: { command: (string | number)[] }) => boolean; - } | null; - getCurrentSubtitleData: () => SubtitleData | null; - getHoveredTokenIndex: () => number | null; - getHoveredSubtitleRevision: () => number; - getHoverTokenColor: () => string | null; -}) { - return (): void => { - const mpvClient = deps.getMpvClient(); - if (!mpvClient || !mpvClient.connected) { - return; - } - - const subtitle = deps.getCurrentSubtitleData(); - const hoveredTokenIndex = deps.getHoveredTokenIndex(); - const revision = deps.getHoveredSubtitleRevision(); - const hoverColor = deps.getHoverTokenColor(); - const payload = buildHoveredTokenPayload({ - subtitle: subtitle && hasHoveredToken(subtitle, hoveredTokenIndex) ? subtitle : null, - hoveredTokenIndex: hoveredTokenIndex, - revision, - hoverColor, - }); - - mpvClient.send({ command: buildHoveredTokenMessageCommand(payload) }); - }; -} diff --git a/src/main/runtime/mpv-main-event-actions.test.ts b/src/main/runtime/mpv-main-event-actions.test.ts index 61ee595..9a923af 100644 --- a/src/main/runtime/mpv-main-event-actions.test.ts +++ b/src/main/runtime/mpv-main-event-actions.test.ts @@ -51,6 +51,7 @@ test('media path change handler reports stop for empty path and probes media key const handler = createHandleMpvMediaPathChangeHandler({ updateCurrentMediaPath: (path) => calls.push(`path:${path}`), reportJellyfinRemoteStopped: () => calls.push('stopped'), + restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'), getCurrentAnilistMediaKey: () => 'show:1', resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`), maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`), @@ -63,6 +64,7 @@ test('media path change handler reports stop for empty path and probes media key assert.deepEqual(calls, [ 'path:', 'stopped', + 'restore-mpv-sub', 'reset:show:1', 'probe:show:1', 'guess:show:1', diff --git a/src/main/runtime/overlay-bootstrap-main-deps.test.ts b/src/main/runtime/overlay-bootstrap-main-deps.test.ts index 8b1f600..4fd86f1 100644 --- a/src/main/runtime/overlay-bootstrap-main-deps.test.ts +++ b/src/main/runtime/overlay-bootstrap-main-deps.test.ts @@ -19,12 +19,10 @@ test('overlay content measurement store main deps builder maps callbacks', () => test('overlay modal runtime main deps builder maps window resolvers', () => { const mainWindow = { id: 'main' }; - const invisibleWindow = { id: 'invisible' }; const modalWindow = { id: 'modal' }; const calls: string[] = []; const deps = createBuildOverlayModalRuntimeMainDepsHandler({ getMainWindow: () => mainWindow as never, - getInvisibleWindow: () => invisibleWindow as never, getModalWindow: () => modalWindow as never, createModalWindow: () => modalWindow as never, getModalGeometry: () => ({ x: 1, y: 2, width: 3, height: 4 }), @@ -33,7 +31,6 @@ test('overlay modal runtime main deps builder maps window resolvers', () => { })(); assert.equal(deps.getMainWindow(), mainWindow); - assert.equal(deps.getInvisibleWindow(), invisibleWindow); assert.equal(deps.getModalWindow(), modalWindow); assert.equal(deps.createModalWindow(), modalWindow); assert.deepEqual(deps.getModalGeometry(), { x: 1, y: 2, width: 3, height: 4 }); diff --git a/src/main/runtime/overlay-bootstrap-main-deps.ts b/src/main/runtime/overlay-bootstrap-main-deps.ts index dc59a34..b16d6c5 100644 --- a/src/main/runtime/overlay-bootstrap-main-deps.ts +++ b/src/main/runtime/overlay-bootstrap-main-deps.ts @@ -19,7 +19,6 @@ export function createBuildOverlayModalRuntimeMainDepsHandler( ) { return (): OverlayWindowResolver => ({ getMainWindow: () => deps.getMainWindow(), - getInvisibleWindow: () => deps.getInvisibleWindow(), getModalWindow: () => deps.getModalWindow(), createModalWindow: () => deps.createModalWindow(), getModalGeometry: () => deps.getModalGeometry(), diff --git a/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts b/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts index 8ce8462..4e73a59 100644 --- a/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts +++ b/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts @@ -15,7 +15,6 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h ankiIntegration: null as unknown, }; let initialized = false; - let invisibleOverlayVisible = false; let warmupsStarted = 0; const { initializeOverlayRuntime } = createOverlayRuntimeBootstrapHandlers({ @@ -23,21 +22,16 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h appState, overlayManager: { getVisibleOverlayVisible: () => true, - getInvisibleOverlayVisible: () => false, }, overlayVisibilityRuntime: { updateVisibleOverlayVisibility: () => {}, - updateInvisibleOverlayVisibility: () => {}, }, overlayShortcutsRuntime: { syncOverlayShortcuts: () => {}, }, - getInitialInvisibleOverlayVisibility: () => false, createMainWindow: () => {}, - createInvisibleWindow: () => {}, registerGlobalShortcuts: () => {}, updateVisibleOverlayBounds: () => {}, - updateInvisibleOverlayBounds: () => {}, getOverlayWindows: () => [], getResolvedConfig: () => ({}), showDesktopNotification: () => {}, @@ -52,10 +46,7 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h }, initializeOverlayRuntimeBootstrapDeps: { isOverlayRuntimeInitialized: () => initialized, - initializeOverlayRuntimeCore: () => ({ invisibleOverlayVisible: true }), - setInvisibleOverlayVisible: (visible) => { - invisibleOverlayVisible = visible; - }, + initializeOverlayRuntimeCore: () => {}, setOverlayRuntimeInitialized: (next) => { initialized = next; }, @@ -68,7 +59,6 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h initializeOverlayRuntime(); initializeOverlayRuntime(); - assert.equal(invisibleOverlayVisible, true); assert.equal(initialized, true); assert.equal(warmupsStarted, 1); }); diff --git a/src/main/runtime/overlay-runtime-bootstrap.test.ts b/src/main/runtime/overlay-runtime-bootstrap.test.ts index afe2228..890313d 100644 --- a/src/main/runtime/overlay-runtime-bootstrap.test.ts +++ b/src/main/runtime/overlay-runtime-bootstrap.test.ts @@ -8,10 +8,8 @@ test('overlay runtime bootstrap no-ops when already initialized', () => { isOverlayRuntimeInitialized: () => true, initializeOverlayRuntimeCore: () => { coreCalls += 1; - return { invisibleOverlayVisible: false }; }, buildOptions: () => ({} as never), - setInvisibleOverlayVisible: () => {}, setOverlayRuntimeInitialized: () => {}, startBackgroundWarmups: () => {}, }); @@ -27,15 +25,11 @@ test('overlay runtime bootstrap runs core init and applies post-init state', () isOverlayRuntimeInitialized: () => initialized, initializeOverlayRuntimeCore: () => { calls.push('core'); - return { invisibleOverlayVisible: true }; }, buildOptions: () => { calls.push('options'); return {} as never; }, - setInvisibleOverlayVisible: (visible) => { - calls.push(`invisible:${visible ? 'yes' : 'no'}`); - }, setOverlayRuntimeInitialized: (value) => { initialized = value; calls.push(`initialized:${value ? 'yes' : 'no'}`); @@ -47,5 +41,5 @@ test('overlay runtime bootstrap runs core init and applies post-init state', () initialize(); assert.equal(initialized, true); - assert.deepEqual(calls, ['options', 'core', 'invisible:yes', 'initialized:yes', 'warmups']); + assert.deepEqual(calls, ['options', 'core', 'initialized:yes', 'warmups']); }); diff --git a/src/main/runtime/overlay-runtime-bootstrap.ts b/src/main/runtime/overlay-runtime-bootstrap.ts index e05f7de..8bac07b 100644 --- a/src/main/runtime/overlay-runtime-bootstrap.ts +++ b/src/main/runtime/overlay-runtime-bootstrap.ts @@ -9,16 +9,11 @@ import type { type InitializeOverlayRuntimeCore = (options: { backendOverride: string | null; - getInitialInvisibleOverlayVisibility: () => boolean; createMainWindow: () => void; - createInvisibleWindow: () => void; registerGlobalShortcuts: () => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; isVisibleOverlayVisible: () => boolean; - isInvisibleOverlayVisible: () => boolean; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; getOverlayWindows: () => BrowserWindow[]; syncOverlayShortcuts: () => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void; @@ -35,20 +30,18 @@ type InitializeOverlayRuntimeCore = (options: { data: KikuFieldGroupingRequestData, ) => Promise; getKnownWordCacheStatePath: () => string; -}) => { invisibleOverlayVisible: boolean }; +}) => void; export function createInitializeOverlayRuntimeHandler(deps: { isOverlayRuntimeInitialized: () => boolean; initializeOverlayRuntimeCore: InitializeOverlayRuntimeCore; buildOptions: () => Parameters[0]; - setInvisibleOverlayVisible: (visible: boolean) => void; setOverlayRuntimeInitialized: (initialized: boolean) => void; startBackgroundWarmups: () => void; }) { return (): void => { if (deps.isOverlayRuntimeInitialized()) return; - const result = deps.initializeOverlayRuntimeCore(deps.buildOptions()); - deps.setInvisibleOverlayVisible(result.invisibleOverlayVisible); + deps.initializeOverlayRuntimeCore(deps.buildOptions()); deps.setOverlayRuntimeInitialized(true); deps.startBackgroundWarmups(); }; diff --git a/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts b/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts index 4e50aef..298e805 100644 --- a/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts +++ b/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts @@ -57,14 +57,12 @@ test('set overlay debug visualization main deps builder maps callbacks', () => { setOverlayDebugVisualizationEnabledRuntime: () => calls.push('set-runtime'), getCurrentEnabled: () => false, setCurrentEnabled: () => calls.push('set-current'), - broadcastToOverlayWindows: () => calls.push('broadcast'), })(); - deps.setOverlayDebugVisualizationEnabledRuntime(false, true, () => {}, () => {}); + deps.setOverlayDebugVisualizationEnabledRuntime(false, true, () => {}); assert.equal(deps.getCurrentEnabled(), false); deps.setCurrentEnabled(true); - deps.broadcastToOverlayWindows('overlay:debug'); - assert.deepEqual(calls, ['set-runtime', 'set-current', 'broadcast']); + assert.deepEqual(calls, ['set-runtime', 'set-current']); }); test('open runtime options palette main deps builder maps callbacks', () => { diff --git a/src/main/runtime/overlay-runtime-main-actions-main-deps.ts b/src/main/runtime/overlay-runtime-main-actions-main-deps.ts index 683c4ee..23dd597 100644 --- a/src/main/runtime/overlay-runtime-main-actions-main-deps.ts +++ b/src/main/runtime/overlay-runtime-main-actions-main-deps.ts @@ -65,18 +65,14 @@ export function createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler( currentEnabled, nextEnabled, setCurrentEnabled, - broadcastToOverlayWindows, ) => deps.setOverlayDebugVisualizationEnabledRuntime( currentEnabled, nextEnabled, setCurrentEnabled, - broadcastToOverlayWindows, ), getCurrentEnabled: () => deps.getCurrentEnabled(), setCurrentEnabled: (enabled: boolean) => deps.setCurrentEnabled(enabled), - broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => - deps.broadcastToOverlayWindows(channel, ...args), }); } diff --git a/src/main/runtime/overlay-runtime-main-actions.test.ts b/src/main/runtime/overlay-runtime-main-actions.test.ts index f08c536..109613e 100644 --- a/src/main/runtime/overlay-runtime-main-actions.test.ts +++ b/src/main/runtime/overlay-runtime-main-actions.test.ts @@ -104,22 +104,21 @@ test('set overlay debug visualization enabled delegates with current state and b const calls: string[] = []; let current = false; const setEnabled = createSetOverlayDebugVisualizationEnabledHandler({ - setOverlayDebugVisualizationEnabledRuntime: (curr, next, setCurrent, broadcast) => { + setOverlayDebugVisualizationEnabledRuntime: (curr, next, setCurrent) => { calls.push(`runtime:${curr}->${next}`); setCurrent(next); - broadcast('overlay-debug:set', next); + // no renderer-level side effects for this legacy debug path. }, getCurrentEnabled: () => current, setCurrentEnabled: (enabled) => { current = enabled; calls.push(`set:${enabled}`); }, - broadcastToOverlayWindows: (channel, value) => calls.push(`emit:${channel}:${value}`), }); setEnabled(true); assert.equal(current, true); - assert.deepEqual(calls, ['runtime:false->true', 'set:true', 'emit:overlay-debug:set:true']); + assert.deepEqual(calls, ['runtime:false->true', 'set:true']); }); test('open runtime options palette handler delegates to runtime', () => { diff --git a/src/main/runtime/overlay-runtime-main-actions.ts b/src/main/runtime/overlay-runtime-main-actions.ts index 574195e..db70b3b 100644 --- a/src/main/runtime/overlay-runtime-main-actions.ts +++ b/src/main/runtime/overlay-runtime-main-actions.ts @@ -65,18 +65,15 @@ export function createSetOverlayDebugVisualizationEnabledHandler(deps: { currentEnabled: boolean, nextEnabled: boolean, setCurrentEnabled: (enabled: boolean) => void, - broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void, ) => void; getCurrentEnabled: () => boolean; setCurrentEnabled: (enabled: boolean) => void; - broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void; }) { return (enabled: boolean): void => { deps.setOverlayDebugVisualizationEnabledRuntime( deps.getCurrentEnabled(), enabled, (next) => deps.setCurrentEnabled(next), - (channel, ...args) => deps.broadcastToOverlayWindows(channel, ...args), ); }; } diff --git a/src/main/runtime/overlay-runtime-options-main-deps.test.ts b/src/main/runtime/overlay-runtime-options-main-deps.test.ts index 8e15506..0a69adb 100644 --- a/src/main/runtime/overlay-runtime-options-main-deps.test.ts +++ b/src/main/runtime/overlay-runtime-options-main-deps.test.ts @@ -19,21 +19,16 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () => appState, overlayManager: { getVisibleOverlayVisible: () => true, - getInvisibleOverlayVisible: () => false, }, overlayVisibilityRuntime: { updateVisibleOverlayVisibility: () => calls.push('update-visible'), - updateInvisibleOverlayVisibility: () => calls.push('update-invisible'), }, overlayShortcutsRuntime: { syncOverlayShortcuts: () => calls.push('sync-shortcuts'), }, - getInitialInvisibleOverlayVisibility: () => true, createMainWindow: () => calls.push('create-main'), - createInvisibleWindow: () => calls.push('create-invisible'), registerGlobalShortcuts: () => calls.push('register-shortcuts'), updateVisibleOverlayBounds: () => calls.push('visible-bounds'), - updateInvisibleOverlayBounds: () => calls.push('invisible-bounds'), getOverlayWindows: () => [], getResolvedConfig: () => ({}), showDesktopNotification: () => calls.push('notify'), @@ -48,19 +43,14 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () => const deps = build(); assert.equal(deps.getBackendOverride(), 'x11'); - assert.equal(deps.getInitialInvisibleOverlayVisibility(), true); assert.equal(deps.isVisibleOverlayVisible(), true); - assert.equal(deps.isInvisibleOverlayVisible(), false); assert.equal(deps.getMpvSocketPath(), '/tmp/mpv.sock'); assert.equal(deps.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json'); deps.createMainWindow(); - deps.createInvisibleWindow(); deps.registerGlobalShortcuts(); deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); - deps.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); deps.updateVisibleOverlayVisibility(); - deps.updateInvisibleOverlayVisibility(); deps.syncOverlayShortcuts(); deps.showDesktopNotification('title', {}); @@ -73,12 +63,9 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () => assert.deepEqual(calls, [ 'create-main', - 'create-invisible', 'register-shortcuts', 'visible-bounds', - 'invisible-bounds', 'update-visible', - 'update-invisible', 'sync-shortcuts', 'notify', ]); diff --git a/src/main/runtime/overlay-runtime-options-main-deps.ts b/src/main/runtime/overlay-runtime-options-main-deps.ts index b088d1b..8baa009 100644 --- a/src/main/runtime/overlay-runtime-options-main-deps.ts +++ b/src/main/runtime/overlay-runtime-options-main-deps.ts @@ -17,18 +17,14 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { }; overlayManager: { getVisibleOverlayVisible: () => boolean; - getInvisibleOverlayVisible: () => boolean; }; overlayVisibilityRuntime: { updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; }; overlayShortcutsRuntime: { syncOverlayShortcuts: () => void; }; - getInitialInvisibleOverlayVisibility: () => boolean; createMainWindow: () => void; - createInvisibleWindow: () => void; registerGlobalShortcuts: () => void; updateVisibleOverlayBounds: (geometry: { x: number; @@ -36,12 +32,6 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { width: number; height: number; }) => void; - updateInvisibleOverlayBounds: (geometry: { - x: number; - y: number; - width: number; - height: number; - }) => void; getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows']; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void; @@ -50,9 +40,7 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { }) { return (): OverlayRuntimeOptionsMainDeps => ({ getBackendOverride: () => deps.appState.backendOverride, - getInitialInvisibleOverlayVisibility: () => deps.getInitialInvisibleOverlayVisibility(), createMainWindow: () => deps.createMainWindow(), - createInvisibleWindow: () => deps.createInvisibleWindow(), registerGlobalShortcuts: () => deps.registerGlobalShortcuts(), updateVisibleOverlayBounds: (geometry: { x: number; @@ -60,18 +48,9 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { width: number; height: number; }) => deps.updateVisibleOverlayBounds(geometry), - updateInvisibleOverlayBounds: (geometry: { - x: number; - y: number; - width: number; - height: number; - }) => deps.updateInvisibleOverlayBounds(geometry), isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(), - isInvisibleOverlayVisible: () => deps.overlayManager.getInvisibleOverlayVisible(), updateVisibleOverlayVisibility: () => deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(), - updateInvisibleOverlayVisibility: () => - deps.overlayVisibilityRuntime.updateInvisibleOverlayVisibility(), getOverlayWindows: () => deps.getOverlayWindows(), syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(), setWindowTracker: (tracker) => { diff --git a/src/main/runtime/overlay-runtime-options.test.ts b/src/main/runtime/overlay-runtime-options.test.ts index 9f8d853..90ff1d3 100644 --- a/src/main/runtime/overlay-runtime-options.test.ts +++ b/src/main/runtime/overlay-runtime-options.test.ts @@ -6,16 +6,11 @@ test('build initialize overlay runtime options maps dependencies', () => { const calls: string[] = []; const buildOptions = createBuildInitializeOverlayRuntimeOptionsHandler({ getBackendOverride: () => 'x11', - getInitialInvisibleOverlayVisibility: () => true, createMainWindow: () => calls.push('create-main'), - createInvisibleWindow: () => calls.push('create-invisible'), registerGlobalShortcuts: () => calls.push('register-shortcuts'), updateVisibleOverlayBounds: () => calls.push('update-visible-bounds'), - updateInvisibleOverlayBounds: () => calls.push('update-invisible-bounds'), isVisibleOverlayVisible: () => true, - isInvisibleOverlayVisible: () => false, updateVisibleOverlayVisibility: () => calls.push('update-visible'), - updateInvisibleOverlayVisibility: () => calls.push('update-invisible'), getOverlayWindows: () => [], syncOverlayShortcuts: () => calls.push('sync-shortcuts'), setWindowTracker: () => calls.push('set-tracker'), @@ -37,18 +32,13 @@ test('build initialize overlay runtime options maps dependencies', () => { const options = buildOptions(); assert.equal(options.backendOverride, 'x11'); - assert.equal(options.getInitialInvisibleOverlayVisibility(), true); assert.equal(options.isVisibleOverlayVisible(), true); - assert.equal(options.isInvisibleOverlayVisible(), false); assert.equal(options.getMpvSocketPath(), '/tmp/mpv.sock'); assert.equal(options.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json'); options.createMainWindow(); - options.createInvisibleWindow(); options.registerGlobalShortcuts(); options.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); - options.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); options.updateVisibleOverlayVisibility(); - options.updateInvisibleOverlayVisibility(); options.syncOverlayShortcuts(); options.setWindowTracker(null); options.setAnkiIntegration(null); @@ -56,12 +46,9 @@ test('build initialize overlay runtime options maps dependencies', () => { assert.deepEqual(calls, [ 'create-main', - 'create-invisible', 'register-shortcuts', 'update-visible-bounds', - 'update-invisible-bounds', 'update-visible', - 'update-invisible', 'sync-shortcuts', 'set-tracker', 'set-anki', diff --git a/src/main/runtime/overlay-runtime-options.ts b/src/main/runtime/overlay-runtime-options.ts index a6873a4..664588b 100644 --- a/src/main/runtime/overlay-runtime-options.ts +++ b/src/main/runtime/overlay-runtime-options.ts @@ -9,16 +9,11 @@ import type { BaseWindowTracker } from '../../window-trackers'; type OverlayRuntimeOptions = { backendOverride: string | null; - getInitialInvisibleOverlayVisibility: () => boolean; createMainWindow: () => void; - createInvisibleWindow: () => void; registerGlobalShortcuts: () => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; isVisibleOverlayVisible: () => boolean; - isInvisibleOverlayVisible: () => boolean; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; getOverlayWindows: () => BrowserWindow[]; syncOverlayShortcuts: () => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void; @@ -39,16 +34,11 @@ type OverlayRuntimeOptions = { export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: { getBackendOverride: () => string | null; - getInitialInvisibleOverlayVisibility: () => boolean; createMainWindow: () => void; - createInvisibleWindow: () => void; registerGlobalShortcuts: () => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void; isVisibleOverlayVisible: () => boolean; - isInvisibleOverlayVisible: () => boolean; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; getOverlayWindows: () => BrowserWindow[]; syncOverlayShortcuts: () => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void; @@ -68,16 +58,11 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: { }) { return (): OverlayRuntimeOptions => ({ backendOverride: deps.getBackendOverride(), - getInitialInvisibleOverlayVisibility: deps.getInitialInvisibleOverlayVisibility, createMainWindow: deps.createMainWindow, - createInvisibleWindow: deps.createInvisibleWindow, registerGlobalShortcuts: deps.registerGlobalShortcuts, updateVisibleOverlayBounds: deps.updateVisibleOverlayBounds, - updateInvisibleOverlayBounds: deps.updateInvisibleOverlayBounds, isVisibleOverlayVisible: deps.isVisibleOverlayVisible, - isInvisibleOverlayVisible: deps.isInvisibleOverlayVisible, updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility, - updateInvisibleOverlayVisibility: deps.updateInvisibleOverlayVisibility, getOverlayWindows: deps.getOverlayWindows, syncOverlayShortcuts: deps.syncOverlayShortcuts, setWindowTracker: deps.setWindowTracker, diff --git a/src/main/runtime/overlay-visibility-actions-main-deps.test.ts b/src/main/runtime/overlay-visibility-actions-main-deps.test.ts index c2f5f05..d3f52dc 100644 --- a/src/main/runtime/overlay-visibility-actions-main-deps.test.ts +++ b/src/main/runtime/overlay-visibility-actions-main-deps.test.ts @@ -1,9 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { - createBuildSetInvisibleOverlayVisibleMainDepsHandler, createBuildSetVisibleOverlayVisibleMainDepsHandler, - createBuildToggleInvisibleOverlayMainDepsHandler, createBuildToggleVisibleOverlayMainDepsHandler, } from './overlay-visibility-actions-main-deps'; @@ -14,45 +12,14 @@ test('overlay visibility action main deps builders map callbacks', () => { setVisibleOverlayVisibleCore: () => calls.push('visible-core'), setVisibleOverlayVisibleState: (visible) => calls.push(`visible-state:${visible}`), updateVisibleOverlayVisibility: () => calls.push('update-visible'), - updateInvisibleOverlayVisibility: () => calls.push('update-invisible'), - syncInvisibleOverlayMousePassthrough: () => calls.push('sync'), - shouldBindVisibleOverlayToMpvSubVisibility: () => true, - isMpvConnected: () => true, - setMpvSubVisibility: (visible) => calls.push(`mpv:${visible}`), })(); setVisible.setVisibleOverlayVisibleCore({ visible: true, setVisibleOverlayVisibleState: () => {}, updateVisibleOverlayVisibility: () => {}, - updateInvisibleOverlayVisibility: () => {}, - syncInvisibleOverlayMousePassthrough: () => {}, - shouldBindVisibleOverlayToMpvSubVisibility: () => true, - isMpvConnected: () => true, - setMpvSubVisibility: () => {}, }); setVisible.setVisibleOverlayVisibleState(true); setVisible.updateVisibleOverlayVisibility(); - setVisible.updateInvisibleOverlayVisibility(); - setVisible.syncInvisibleOverlayMousePassthrough(); - assert.equal(setVisible.shouldBindVisibleOverlayToMpvSubVisibility(), true); - assert.equal(setVisible.isMpvConnected(), true); - setVisible.setMpvSubVisibility(false); - - const setInvisible = createBuildSetInvisibleOverlayVisibleMainDepsHandler({ - setInvisibleOverlayVisibleCore: () => calls.push('invisible-core'), - setInvisibleOverlayVisibleState: (visible) => calls.push(`invisible-state:${visible}`), - updateInvisibleOverlayVisibility: () => calls.push('update-only-invisible'), - syncInvisibleOverlayMousePassthrough: () => calls.push('sync-only'), - })(); - setInvisible.setInvisibleOverlayVisibleCore({ - visible: false, - setInvisibleOverlayVisibleState: () => {}, - updateInvisibleOverlayVisibility: () => {}, - syncInvisibleOverlayMousePassthrough: () => {}, - }); - setInvisible.setInvisibleOverlayVisibleState(false); - setInvisible.updateInvisibleOverlayVisibility(); - setInvisible.syncInvisibleOverlayMousePassthrough(); const toggleVisible = createBuildToggleVisibleOverlayMainDepsHandler({ getVisibleOverlayVisible: () => false, @@ -61,25 +28,10 @@ test('overlay visibility action main deps builders map callbacks', () => { assert.equal(toggleVisible.getVisibleOverlayVisible(), false); toggleVisible.setVisibleOverlayVisible(true); - const toggleInvisible = createBuildToggleInvisibleOverlayMainDepsHandler({ - getInvisibleOverlayVisible: () => true, - setInvisibleOverlayVisible: (visible) => calls.push(`toggle-invisible:${visible}`), - })(); - assert.equal(toggleInvisible.getInvisibleOverlayVisible(), true); - toggleInvisible.setInvisibleOverlayVisible(false); - assert.deepEqual(calls, [ 'visible-core', 'visible-state:true', 'update-visible', - 'update-invisible', - 'sync', - 'mpv:false', - 'invisible-core', - 'invisible-state:false', - 'update-only-invisible', - 'sync-only', 'toggle-visible:true', - 'toggle-invisible:false', ]); }); diff --git a/src/main/runtime/overlay-visibility-actions-main-deps.ts b/src/main/runtime/overlay-visibility-actions-main-deps.ts index f4b9941..a707f8e 100644 --- a/src/main/runtime/overlay-visibility-actions-main-deps.ts +++ b/src/main/runtime/overlay-visibility-actions-main-deps.ts @@ -1,14 +1,10 @@ import type { - createSetInvisibleOverlayVisibleHandler, createSetVisibleOverlayVisibleHandler, - createToggleInvisibleOverlayHandler, createToggleVisibleOverlayHandler, } from './overlay-visibility-actions'; type SetVisibleOverlayVisibleMainDeps = Parameters[0]; -type SetInvisibleOverlayVisibleMainDeps = Parameters[0]; type ToggleVisibleOverlayMainDeps = Parameters[0]; -type ToggleInvisibleOverlayMainDeps = Parameters[0]; export function createBuildSetVisibleOverlayVisibleMainDepsHandler( deps: SetVisibleOverlayVisibleMainDeps, @@ -17,22 +13,6 @@ export function createBuildSetVisibleOverlayVisibleMainDepsHandler( setVisibleOverlayVisibleCore: (options) => deps.setVisibleOverlayVisibleCore(options), setVisibleOverlayVisibleState: (visible: boolean) => deps.setVisibleOverlayVisibleState(visible), updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), - updateInvisibleOverlayVisibility: () => deps.updateInvisibleOverlayVisibility(), - syncInvisibleOverlayMousePassthrough: () => deps.syncInvisibleOverlayMousePassthrough(), - shouldBindVisibleOverlayToMpvSubVisibility: () => deps.shouldBindVisibleOverlayToMpvSubVisibility(), - isMpvConnected: () => deps.isMpvConnected(), - setMpvSubVisibility: (visible: boolean) => deps.setMpvSubVisibility(visible), - }); -} - -export function createBuildSetInvisibleOverlayVisibleMainDepsHandler( - deps: SetInvisibleOverlayVisibleMainDeps, -) { - return (): SetInvisibleOverlayVisibleMainDeps => ({ - setInvisibleOverlayVisibleCore: (options) => deps.setInvisibleOverlayVisibleCore(options), - setInvisibleOverlayVisibleState: (visible: boolean) => deps.setInvisibleOverlayVisibleState(visible), - updateInvisibleOverlayVisibility: () => deps.updateInvisibleOverlayVisibility(), - syncInvisibleOverlayMousePassthrough: () => deps.syncInvisibleOverlayMousePassthrough(), }); } @@ -42,12 +22,3 @@ export function createBuildToggleVisibleOverlayMainDepsHandler(deps: ToggleVisib setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible), }); } - -export function createBuildToggleInvisibleOverlayMainDepsHandler( - deps: ToggleInvisibleOverlayMainDeps, -) { - return (): ToggleInvisibleOverlayMainDeps => ({ - getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), - setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), - }); -} diff --git a/src/main/runtime/overlay-visibility-actions.test.ts b/src/main/runtime/overlay-visibility-actions.test.ts index afaae12..068f087 100644 --- a/src/main/runtime/overlay-visibility-actions.test.ts +++ b/src/main/runtime/overlay-visibility-actions.test.ts @@ -1,9 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { - createSetInvisibleOverlayVisibleHandler, createSetVisibleOverlayVisibleHandler, - createToggleInvisibleOverlayHandler, createToggleVisibleOverlayHandler, } from './overlay-visibility-actions'; @@ -14,17 +12,9 @@ test('set visible overlay handler forwards dependencies to core', () => { calls.push(`core:${options.visible}`); options.setVisibleOverlayVisibleState(options.visible); options.updateVisibleOverlayVisibility(); - options.updateInvisibleOverlayVisibility(); - options.syncInvisibleOverlayMousePassthrough(); - options.setMpvSubVisibility(!options.visible); }, setVisibleOverlayVisibleState: (visible) => calls.push(`state:${visible}`), updateVisibleOverlayVisibility: () => calls.push('update-visible'), - updateInvisibleOverlayVisibility: () => calls.push('update-invisible'), - syncInvisibleOverlayMousePassthrough: () => calls.push('sync-mouse'), - shouldBindVisibleOverlayToMpvSubVisibility: () => true, - isMpvConnected: () => true, - setMpvSubVisibility: (visible) => calls.push(`mpv-sub:${visible}`), }); setVisible(true); @@ -32,30 +22,9 @@ test('set visible overlay handler forwards dependencies to core', () => { 'core:true', 'state:true', 'update-visible', - 'update-invisible', - 'sync-mouse', - 'mpv-sub:false', ]); }); -test('set invisible overlay handler forwards dependencies to core', () => { - const calls: string[] = []; - const setInvisible = createSetInvisibleOverlayVisibleHandler({ - setInvisibleOverlayVisibleCore: (options) => { - calls.push(`core:${options.visible}`); - options.setInvisibleOverlayVisibleState(options.visible); - options.updateInvisibleOverlayVisibility(); - options.syncInvisibleOverlayMousePassthrough(); - }, - setInvisibleOverlayVisibleState: (visible) => calls.push(`state:${visible}`), - updateInvisibleOverlayVisibility: () => calls.push('update-invisible'), - syncInvisibleOverlayMousePassthrough: () => calls.push('sync-mouse'), - }); - - setInvisible(false); - assert.deepEqual(calls, ['core:false', 'state:false', 'update-invisible', 'sync-mouse']); -}); - test('toggle visible overlay flips current visible state', () => { const calls: string[] = []; let current = false; @@ -71,19 +40,3 @@ test('toggle visible overlay flips current visible state', () => { toggle(); assert.deepEqual(calls, ['set:true', 'set:false']); }); - -test('toggle invisible overlay flips current invisible state', () => { - const calls: string[] = []; - let current = true; - const toggle = createToggleInvisibleOverlayHandler({ - getInvisibleOverlayVisible: () => current, - setInvisibleOverlayVisible: (visible) => { - current = visible; - calls.push(`set:${visible}`); - }, - }); - - toggle(); - toggle(); - assert.deepEqual(calls, ['set:false', 'set:true']); -}); diff --git a/src/main/runtime/overlay-visibility-actions.ts b/src/main/runtime/overlay-visibility-actions.ts index a43cd15..67f1637 100644 --- a/src/main/runtime/overlay-visibility-actions.ts +++ b/src/main/runtime/overlay-visibility-actions.ts @@ -3,52 +3,15 @@ export function createSetVisibleOverlayVisibleHandler(deps: { visible: boolean; setVisibleOverlayVisibleState: (visible: boolean) => void; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; - syncInvisibleOverlayMousePassthrough: () => void; - shouldBindVisibleOverlayToMpvSubVisibility: () => boolean; - isMpvConnected: () => boolean; - setMpvSubVisibility: (visible: boolean) => void; }) => void; setVisibleOverlayVisibleState: (visible: boolean) => void; updateVisibleOverlayVisibility: () => void; - updateInvisibleOverlayVisibility: () => void; - syncInvisibleOverlayMousePassthrough: () => void; - shouldBindVisibleOverlayToMpvSubVisibility: () => boolean; - isMpvConnected: () => boolean; - setMpvSubVisibility: (visible: boolean) => void; }) { return (visible: boolean): void => { deps.setVisibleOverlayVisibleCore({ visible, setVisibleOverlayVisibleState: deps.setVisibleOverlayVisibleState, updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility, - updateInvisibleOverlayVisibility: deps.updateInvisibleOverlayVisibility, - syncInvisibleOverlayMousePassthrough: deps.syncInvisibleOverlayMousePassthrough, - shouldBindVisibleOverlayToMpvSubVisibility: - deps.shouldBindVisibleOverlayToMpvSubVisibility, - isMpvConnected: deps.isMpvConnected, - setMpvSubVisibility: deps.setMpvSubVisibility, - }); - }; -} - -export function createSetInvisibleOverlayVisibleHandler(deps: { - setInvisibleOverlayVisibleCore: (options: { - visible: boolean; - setInvisibleOverlayVisibleState: (visible: boolean) => void; - updateInvisibleOverlayVisibility: () => void; - syncInvisibleOverlayMousePassthrough: () => void; - }) => void; - setInvisibleOverlayVisibleState: (visible: boolean) => void; - updateInvisibleOverlayVisibility: () => void; - syncInvisibleOverlayMousePassthrough: () => void; -}) { - return (visible: boolean): void => { - deps.setInvisibleOverlayVisibleCore({ - visible, - setInvisibleOverlayVisibleState: deps.setInvisibleOverlayVisibleState, - updateInvisibleOverlayVisibility: deps.updateInvisibleOverlayVisibility, - syncInvisibleOverlayMousePassthrough: deps.syncInvisibleOverlayMousePassthrough, }); }; } @@ -61,12 +24,3 @@ export function createToggleVisibleOverlayHandler(deps: { deps.setVisibleOverlayVisible(!deps.getVisibleOverlayVisible()); }; } - -export function createToggleInvisibleOverlayHandler(deps: { - getInvisibleOverlayVisible: () => boolean; - setInvisibleOverlayVisible: (visible: boolean) => void; -}) { - return (): void => { - deps.setInvisibleOverlayVisible(!deps.getInvisibleOverlayVisible()); - }; -} diff --git a/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts b/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts index 37041ec..ba4a823 100644 --- a/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts +++ b/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts @@ -8,14 +8,11 @@ test('overlay visibility runtime main deps builder maps state and geometry callb const calls: string[] = []; let trackerNotReadyWarningShown = false; const mainWindow = { id: 'main' } as never; - const invisibleWindow = { id: 'invisible' } as never; const tracker = { id: 'tracker' } as unknown as BaseWindowTracker; const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({ getMainWindow: () => mainWindow, - getInvisibleWindow: () => invisibleWindow, getVisibleOverlayVisible: () => true, - getInvisibleOverlayVisible: () => false, getWindowTracker: () => tracker, getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown, setTrackerNotReadyWarningShown: (shown) => { @@ -23,30 +20,35 @@ test('overlay visibility runtime main deps builder maps state and geometry callb calls.push(`tracker-warning:${shown}`); }, updateVisibleOverlayBounds: () => calls.push('visible-bounds'), - updateInvisibleOverlayBounds: () => calls.push('invisible-bounds'), ensureOverlayWindowLevel: () => calls.push('ensure-level'), + syncPrimaryOverlayWindowLayer: (layer) => calls.push(`primary-layer:${layer}`), enforceOverlayLayerOrder: () => calls.push('enforce-order'), syncOverlayShortcuts: () => calls.push('sync-shortcuts'), + isMacOSPlatform: () => true, + showOverlayLoadingOsd: () => calls.push('overlay-loading-osd'), + resolveFallbackBounds: () => ({ x: 0, y: 0, width: 20, height: 20 }), })(); assert.equal(deps.getMainWindow(), mainWindow); - assert.equal(deps.getInvisibleWindow(), invisibleWindow); assert.equal(deps.getVisibleOverlayVisible(), true); - assert.equal(deps.getInvisibleOverlayVisible(), false); assert.equal(deps.getTrackerNotReadyWarningShown(), false); deps.setTrackerNotReadyWarningShown(true); deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); - deps.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); deps.ensureOverlayWindowLevel(mainWindow); + deps.syncPrimaryOverlayWindowLayer('visible'); deps.enforceOverlayLayerOrder(); deps.syncOverlayShortcuts(); + assert.equal(deps.isMacOSPlatform(), true); + deps.showOverlayLoadingOsd('Overlay loading...'); + assert.deepEqual(deps.resolveFallbackBounds(), { x: 0, y: 0, width: 20, height: 20 }); assert.equal(trackerNotReadyWarningShown, true); assert.deepEqual(calls, [ 'tracker-warning:true', 'visible-bounds', - 'invisible-bounds', 'ensure-level', + 'primary-layer:visible', 'enforce-order', 'sync-shortcuts', + 'overlay-loading-osd', ]); }); diff --git a/src/main/runtime/overlay-visibility-runtime-main-deps.ts b/src/main/runtime/overlay-visibility-runtime-main-deps.ts index 466ebe3..85c996d 100644 --- a/src/main/runtime/overlay-visibility-runtime-main-deps.ts +++ b/src/main/runtime/overlay-visibility-runtime-main-deps.ts @@ -7,18 +7,19 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler( ) { return (): OverlayVisibilityRuntimeDeps => ({ getMainWindow: () => deps.getMainWindow(), - getInvisibleWindow: () => deps.getInvisibleWindow(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), - getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), getWindowTracker: () => deps.getWindowTracker(), getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(), setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown), updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry), - updateInvisibleOverlayBounds: (geometry: WindowGeometry) => - deps.updateInvisibleOverlayBounds(geometry), ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), + syncPrimaryOverlayWindowLayer: (layer: 'visible') => + deps.syncPrimaryOverlayWindowLayer(layer), enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(), syncOverlayShortcuts: () => deps.syncOverlayShortcuts(), + isMacOSPlatform: () => deps.isMacOSPlatform(), + showOverlayLoadingOsd: (message: string) => deps.showOverlayLoadingOsd(message), + resolveFallbackBounds: () => deps.resolveFallbackBounds(), }); } diff --git a/src/main/runtime/overlay-visibility-runtime.test.ts b/src/main/runtime/overlay-visibility-runtime.test.ts index de65798..1b7b3b5 100644 --- a/src/main/runtime/overlay-visibility-runtime.test.ts +++ b/src/main/runtime/overlay-visibility-runtime.test.ts @@ -4,10 +4,7 @@ import { createOverlayVisibilityRuntime } from './overlay-visibility-runtime'; test('overlay visibility runtime wires set/toggle handlers through composed deps', () => { let visible = false; - let invisible = true; let setVisibleCoreCalls = 0; - let setInvisibleCoreCalls = 0; - let lastBoundSubVisibility: boolean | null = null; const runtime = createOverlayVisibilityRuntime({ setVisibleOverlayVisibleDeps: { @@ -15,44 +12,17 @@ test('overlay visibility runtime wires set/toggle handlers through composed deps setVisibleCoreCalls += 1; options.setVisibleOverlayVisibleState(options.visible); options.updateVisibleOverlayVisibility(); - options.updateInvisibleOverlayVisibility(); - options.syncInvisibleOverlayMousePassthrough(); - if (options.shouldBindVisibleOverlayToMpvSubVisibility() && options.isMpvConnected()) { - options.setMpvSubVisibility(options.visible); - } }, setVisibleOverlayVisibleState: (nextVisible) => { visible = nextVisible; }, updateVisibleOverlayVisibility: () => {}, - updateInvisibleOverlayVisibility: () => {}, - syncInvisibleOverlayMousePassthrough: () => {}, - shouldBindVisibleOverlayToMpvSubVisibility: () => true, - isMpvConnected: () => true, - setMpvSubVisibility: (nextVisible) => { - lastBoundSubVisibility = nextVisible; - }, - }, - setInvisibleOverlayVisibleDeps: { - setInvisibleOverlayVisibleCore: (options) => { - setInvisibleCoreCalls += 1; - options.setInvisibleOverlayVisibleState(options.visible); - options.updateInvisibleOverlayVisibility(); - options.syncInvisibleOverlayMousePassthrough(); - }, - setInvisibleOverlayVisibleState: (nextVisible) => { - invisible = nextVisible; - }, - updateInvisibleOverlayVisibility: () => {}, - syncInvisibleOverlayMousePassthrough: () => {}, }, getVisibleOverlayVisible: () => visible, - getInvisibleOverlayVisible: () => invisible, }); runtime.setVisibleOverlayVisible(true); assert.equal(visible, true); - assert.equal(lastBoundSubVisibility, true); runtime.toggleVisibleOverlay(); assert.equal(visible, false); @@ -63,12 +33,5 @@ test('overlay visibility runtime wires set/toggle handlers through composed deps runtime.toggleOverlay(); assert.equal(visible, false); - runtime.setInvisibleOverlayVisible(false); - assert.equal(invisible, false); - - runtime.toggleInvisibleOverlay(); - assert.equal(invisible, true); - assert.equal(setVisibleCoreCalls, 4); - assert.equal(setInvisibleCoreCalls, 2); }); diff --git a/src/main/runtime/overlay-visibility-runtime.ts b/src/main/runtime/overlay-visibility-runtime.ts index 9a6db26..87e78b5 100644 --- a/src/main/runtime/overlay-visibility-runtime.ts +++ b/src/main/runtime/overlay-visibility-runtime.ts @@ -1,13 +1,9 @@ import { - createSetInvisibleOverlayVisibleHandler, createSetVisibleOverlayVisibleHandler, - createToggleInvisibleOverlayHandler, createToggleVisibleOverlayHandler, } from './overlay-visibility-actions'; import { - createBuildSetInvisibleOverlayVisibleMainDepsHandler, createBuildSetVisibleOverlayVisibleMainDepsHandler, - createBuildToggleInvisibleOverlayMainDepsHandler, createBuildToggleVisibleOverlayMainDepsHandler, } from './overlay-visibility-actions-main-deps'; import { createSetOverlayVisibleHandler, createToggleOverlayHandler } from './overlay-main-actions'; @@ -19,15 +15,10 @@ import { type SetVisibleOverlayVisibleMainDeps = Parameters< typeof createBuildSetVisibleOverlayVisibleMainDepsHandler >[0]; -type SetInvisibleOverlayVisibleMainDeps = Parameters< - typeof createBuildSetInvisibleOverlayVisibleMainDepsHandler ->[0]; export type OverlayVisibilityRuntimeDeps = { setVisibleOverlayVisibleDeps: SetVisibleOverlayVisibleMainDeps; - setInvisibleOverlayVisibleDeps: SetInvisibleOverlayVisibleMainDeps; getVisibleOverlayVisible: () => boolean; - getInvisibleOverlayVisible: () => boolean; }; export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDeps) { @@ -38,25 +29,12 @@ export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDep setVisibleOverlayVisibleMainDeps, ); - const setInvisibleOverlayVisibleMainDeps = createBuildSetInvisibleOverlayVisibleMainDepsHandler( - deps.setInvisibleOverlayVisibleDeps, - )(); - const setInvisibleOverlayVisible = createSetInvisibleOverlayVisibleHandler( - setInvisibleOverlayVisibleMainDeps, - ); - const toggleVisibleOverlayMainDeps = createBuildToggleVisibleOverlayMainDepsHandler({ getVisibleOverlayVisible: deps.getVisibleOverlayVisible, setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), })(); const toggleVisibleOverlay = createToggleVisibleOverlayHandler(toggleVisibleOverlayMainDeps); - const toggleInvisibleOverlayMainDeps = createBuildToggleInvisibleOverlayMainDepsHandler({ - getInvisibleOverlayVisible: deps.getInvisibleOverlayVisible, - setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible), - })(); - const toggleInvisibleOverlay = createToggleInvisibleOverlayHandler(toggleInvisibleOverlayMainDeps); - const setOverlayVisibleMainDeps = createBuildSetOverlayVisibleMainDepsHandler({ setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), })(); @@ -69,9 +47,7 @@ export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDep return { setVisibleOverlayVisible, - setInvisibleOverlayVisible, toggleVisibleOverlay, - toggleInvisibleOverlay, setOverlayVisible, toggleOverlay, }; diff --git a/src/main/runtime/overlay-window-layout-main-deps.test.ts b/src/main/runtime/overlay-window-layout-main-deps.test.ts index 4ced712..3b38dac 100644 --- a/src/main/runtime/overlay-window-layout-main-deps.test.ts +++ b/src/main/runtime/overlay-window-layout-main-deps.test.ts @@ -3,7 +3,6 @@ import test from 'node:test'; import { createBuildEnforceOverlayLayerOrderMainDepsHandler, createBuildEnsureOverlayWindowLevelMainDepsHandler, - createBuildUpdateInvisibleOverlayBoundsMainDepsHandler, createBuildUpdateVisibleOverlayBoundsMainDepsHandler, } from './overlay-window-layout-main-deps'; @@ -11,14 +10,9 @@ test('overlay window layout main deps builders map callbacks', () => { const calls: string[] = []; const visible = createBuildUpdateVisibleOverlayBoundsMainDepsHandler({ - setOverlayWindowBounds: (layer) => calls.push(`visible:${layer}`), + setOverlayWindowBounds: () => calls.push('visible'), })(); - visible.setOverlayWindowBounds('visible', { x: 0, y: 0, width: 1, height: 1 }); - - const invisible = createBuildUpdateInvisibleOverlayBoundsMainDepsHandler({ - setOverlayWindowBounds: (layer) => calls.push(`invisible:${layer}`), - })(); - invisible.setOverlayWindowBounds('invisible', { x: 0, y: 0, width: 1, height: 1 }); + visible.setOverlayWindowBounds({ x: 0, y: 0, width: 1, height: 1 }); const level = createBuildEnsureOverlayWindowLevelMainDepsHandler({ ensureOverlayWindowLevelCore: () => calls.push('ensure'), @@ -28,27 +22,20 @@ test('overlay window layout main deps builders map callbacks', () => { const order = createBuildEnforceOverlayLayerOrderMainDepsHandler({ enforceOverlayLayerOrderCore: () => calls.push('order'), getVisibleOverlayVisible: () => true, - getInvisibleOverlayVisible: () => false, getMainWindow: () => ({ kind: 'main' }), - getInvisibleWindow: () => ({ kind: 'invisible' }), ensureOverlayWindowLevel: () => calls.push('order-level'), })(); order.enforceOverlayLayerOrderCore({ visibleOverlayVisible: true, - invisibleOverlayVisible: false, mainWindow: null, - invisibleWindow: null, ensureOverlayWindowLevel: () => {}, }); assert.equal(order.getVisibleOverlayVisible(), true); - assert.equal(order.getInvisibleOverlayVisible(), false); assert.deepEqual(order.getMainWindow(), { kind: 'main' }); - assert.deepEqual(order.getInvisibleWindow(), { kind: 'invisible' }); order.ensureOverlayWindowLevel({}); assert.deepEqual(calls, [ - 'visible:visible', - 'invisible:invisible', + 'visible', 'ensure', 'order', 'order-level', diff --git a/src/main/runtime/overlay-window-layout-main-deps.ts b/src/main/runtime/overlay-window-layout-main-deps.ts index 317cacd..fba3348 100644 --- a/src/main/runtime/overlay-window-layout-main-deps.ts +++ b/src/main/runtime/overlay-window-layout-main-deps.ts @@ -1,12 +1,10 @@ import type { createEnforceOverlayLayerOrderHandler, createEnsureOverlayWindowLevelHandler, - createUpdateInvisibleOverlayBoundsHandler, createUpdateVisibleOverlayBoundsHandler, } from './overlay-window-layout'; type UpdateVisibleOverlayBoundsMainDeps = Parameters[0]; -type UpdateInvisibleOverlayBoundsMainDeps = Parameters[0]; type EnsureOverlayWindowLevelMainDeps = Parameters[0]; type EnforceOverlayLayerOrderMainDeps = Parameters[0]; @@ -14,15 +12,7 @@ export function createBuildUpdateVisibleOverlayBoundsMainDepsHandler( deps: UpdateVisibleOverlayBoundsMainDeps, ) { return (): UpdateVisibleOverlayBoundsMainDeps => ({ - setOverlayWindowBounds: (layer, geometry) => deps.setOverlayWindowBounds(layer, geometry), - }); -} - -export function createBuildUpdateInvisibleOverlayBoundsMainDepsHandler( - deps: UpdateInvisibleOverlayBoundsMainDeps, -) { - return (): UpdateInvisibleOverlayBoundsMainDeps => ({ - setOverlayWindowBounds: (layer, geometry) => deps.setOverlayWindowBounds(layer, geometry), + setOverlayWindowBounds: (geometry) => deps.setOverlayWindowBounds(geometry), }); } @@ -40,9 +30,7 @@ export function createBuildEnforceOverlayLayerOrderMainDepsHandler( return (): EnforceOverlayLayerOrderMainDeps => ({ enforceOverlayLayerOrderCore: (params) => deps.enforceOverlayLayerOrderCore(params), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), - getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), getMainWindow: () => deps.getMainWindow(), - getInvisibleWindow: () => deps.getInvisibleWindow(), ensureOverlayWindowLevel: (window: unknown) => deps.ensureOverlayWindowLevel(window), }); } diff --git a/src/main/runtime/overlay-window-layout.test.ts b/src/main/runtime/overlay-window-layout.test.ts index 8eeab17..b1c281f 100644 --- a/src/main/runtime/overlay-window-layout.test.ts +++ b/src/main/runtime/overlay-window-layout.test.ts @@ -3,26 +3,17 @@ import assert from 'node:assert/strict'; import { createEnforceOverlayLayerOrderHandler, createEnsureOverlayWindowLevelHandler, - createUpdateInvisibleOverlayBoundsHandler, createUpdateVisibleOverlayBoundsHandler, } from './overlay-window-layout'; test('visible bounds handler writes visible layer geometry', () => { - const calls: string[] = []; + const calls: Array<{ x: number; y: number; width: number; height: number }> = []; const handleVisible = createUpdateVisibleOverlayBoundsHandler({ - setOverlayWindowBounds: (layer) => calls.push(layer), + setOverlayWindowBounds: (geometry) => calls.push(geometry), }); - handleVisible({ x: 0, y: 0, width: 100, height: 50 }); - assert.deepEqual(calls, ['visible']); -}); - -test('invisible bounds handler writes invisible layer geometry', () => { - const calls: string[] = []; - const handleInvisible = createUpdateInvisibleOverlayBoundsHandler({ - setOverlayWindowBounds: (layer) => calls.push(layer), - }); - handleInvisible({ x: 0, y: 0, width: 100, height: 50 }); - assert.deepEqual(calls, ['invisible']); + const geometry = { x: 0, y: 0, width: 100, height: 50 }; + handleVisible(geometry); + assert.deepEqual(calls, [geometry]); }); test('ensure overlay window level handler delegates to core', () => { @@ -39,15 +30,12 @@ test('enforce overlay layer order handler forwards resolved state', () => { const enforce = createEnforceOverlayLayerOrderHandler({ enforceOverlayLayerOrderCore: (params) => { calls.push(params.visibleOverlayVisible ? 'visible-on' : 'visible-off'); - calls.push(params.invisibleOverlayVisible ? 'invisible-on' : 'invisible-off'); params.ensureOverlayWindowLevel({}); }, getVisibleOverlayVisible: () => true, - getInvisibleOverlayVisible: () => false, getMainWindow: () => ({}), - getInvisibleWindow: () => ({}), ensureOverlayWindowLevel: () => calls.push('ensure-level'), }); enforce(); - assert.deepEqual(calls, ['visible-on', 'invisible-off', 'ensure-level']); + assert.deepEqual(calls, ['visible-on', 'ensure-level']); }); diff --git a/src/main/runtime/overlay-window-layout.ts b/src/main/runtime/overlay-window-layout.ts index 4d62f44..f14da48 100644 --- a/src/main/runtime/overlay-window-layout.ts +++ b/src/main/runtime/overlay-window-layout.ts @@ -1,18 +1,10 @@ import type { WindowGeometry } from '../../types'; export function createUpdateVisibleOverlayBoundsHandler(deps: { - setOverlayWindowBounds: (layer: 'visible' | 'invisible', geometry: WindowGeometry) => void; + setOverlayWindowBounds: (geometry: WindowGeometry) => void; }) { return (geometry: WindowGeometry): void => { - deps.setOverlayWindowBounds('visible', geometry); - }; -} - -export function createUpdateInvisibleOverlayBoundsHandler(deps: { - setOverlayWindowBounds: (layer: 'visible' | 'invisible', geometry: WindowGeometry) => void; -}) { - return (geometry: WindowGeometry): void => { - deps.setOverlayWindowBounds('invisible', geometry); + deps.setOverlayWindowBounds(geometry); }; } @@ -27,23 +19,17 @@ export function createEnsureOverlayWindowLevelHandler(deps: { export function createEnforceOverlayLayerOrderHandler(deps: { enforceOverlayLayerOrderCore: (params: { visibleOverlayVisible: boolean; - invisibleOverlayVisible: boolean; mainWindow: unknown; - invisibleWindow: unknown; ensureOverlayWindowLevel: (window: unknown) => void; }) => void; getVisibleOverlayVisible: () => boolean; - getInvisibleOverlayVisible: () => boolean; getMainWindow: () => unknown; - getInvisibleWindow: () => unknown; ensureOverlayWindowLevel: (window: unknown) => void; }) { return (): void => { deps.enforceOverlayLayerOrderCore({ visibleOverlayVisible: deps.getVisibleOverlayVisible(), - invisibleOverlayVisible: deps.getInvisibleOverlayVisible(), mainWindow: deps.getMainWindow(), - invisibleWindow: deps.getInvisibleWindow(), ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel, }); }; diff --git a/src/main/runtime/runtime-bootstrap-main-deps.test.ts b/src/main/runtime/runtime-bootstrap-main-deps.test.ts index 8b53c16..d3fdca2 100644 --- a/src/main/runtime/runtime-bootstrap-main-deps.test.ts +++ b/src/main/runtime/runtime-bootstrap-main-deps.test.ts @@ -64,7 +64,6 @@ test('config derived runtime main deps builder maps callbacks', () => { const deps = createBuildConfigDerivedRuntimeMainDepsHandler({ getResolvedConfig: () => ({ jimaku: {} } as never), getRuntimeOptionsManager: () => null, - platform: 'darwin', defaultJimakuLanguagePreference: 'ja', defaultJimakuMaxEntryResults: 20, defaultJimakuApiBaseUrl: 'https://api.example.com', @@ -72,7 +71,6 @@ test('config derived runtime main deps builder maps callbacks', () => { assert.deepEqual(deps.getResolvedConfig(), { jimaku: {} }); assert.equal(deps.getRuntimeOptionsManager(), null); - assert.equal(deps.platform, 'darwin'); assert.equal(deps.defaultJimakuLanguagePreference, 'ja'); assert.equal(deps.defaultJimakuMaxEntryResults, 20); assert.equal(deps.defaultJimakuApiBaseUrl, 'https://api.example.com'); diff --git a/src/main/runtime/runtime-bootstrap-main-deps.ts b/src/main/runtime/runtime-bootstrap-main-deps.ts index 015f55f..6fe7600 100644 --- a/src/main/runtime/runtime-bootstrap-main-deps.ts +++ b/src/main/runtime/runtime-bootstrap-main-deps.ts @@ -35,7 +35,6 @@ export function createBuildConfigDerivedRuntimeMainDepsHandler(deps: ConfigDeriv return (): ConfigDerivedRuntimeDeps => ({ getResolvedConfig: () => deps.getResolvedConfig(), getRuntimeOptionsManager: () => deps.getRuntimeOptionsManager(), - platform: deps.platform, defaultJimakuLanguagePreference: deps.defaultJimakuLanguagePreference, defaultJimakuMaxEntryResults: deps.defaultJimakuMaxEntryResults, defaultJimakuApiBaseUrl: deps.defaultJimakuApiBaseUrl, diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index c177222..7598aa0 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -1,5 +1,6 @@ import type { Keybinding } from '../../types'; import type { RendererContext } from '../context'; +import { hasYomitanPopupIframe, isYomitanPopupIframe } from '../yomitan-popup.js'; export function createKeyboardHandlers( ctx: RendererContext, @@ -14,11 +15,6 @@ export function createKeyboardHandlers( fallbackUsed: boolean; fallbackUnavailable: boolean; }) => void; - saveInvisiblePositionEdit: () => void; - cancelInvisiblePositionEdit: () => void; - setInvisiblePositionEditMode: (enabled: boolean) => void; - applyInvisibleSubtitleOffsetPosition: () => void; - updateInvisiblePositionEditHud: () => void; appendClipboardVideoToQueue: () => void; }, ) { @@ -32,9 +28,6 @@ export function createKeyboardHandlers( ['KeyS', { type: 'mpv', command: ['script-message', 'subminer-start'] }], ['Shift+KeyS', { type: 'mpv', command: ['script-message', 'subminer-stop'] }], ['KeyT', { type: 'mpv', command: ['script-message', 'subminer-toggle'] }], - ['KeyI', { type: 'mpv', command: ['script-message', 'subminer-toggle-invisible'] }], - ['Shift+KeyI', { type: 'mpv', command: ['script-message', 'subminer-show-invisible'] }], - ['KeyU', { type: 'mpv', command: ['script-message', 'subminer-hide-invisible'] }], ['KeyO', { type: 'mpv', command: ['script-message', 'subminer-options'] }], ['KeyR', { type: 'mpv', command: ['script-message', 'subminer-restart'] }], ['KeyC', { type: 'mpv', command: ['script-message', 'subminer-status'] }], @@ -46,10 +39,9 @@ export function createKeyboardHandlers( if (!(target instanceof Element)) return false; if (target.closest('.modal')) return true; if (ctx.dom.subtitleContainer.contains(target)) return true; - if (target.tagName === 'IFRAME' && target.id?.startsWith('yomitan-popup')) { + if (isYomitanPopupIframe(target)) return true; + if (target.closest && target.closest('iframe.yomitan-popup, iframe[id^="yomitan-popup"]')) return true; - } - if (target.closest && target.closest('iframe[id^="yomitan-popup"]')) return true; return false; } @@ -63,15 +55,6 @@ export function createKeyboardHandlers( return parts.join('+'); } - function isInvisiblePositionToggleShortcut(e: KeyboardEvent): boolean { - return ( - e.code === ctx.platform.invisiblePositionEditToggleCode && - !e.altKey && - e.shiftKey && - (e.ctrlKey || e.metaKey) - ); - } - function resolveSessionHelpChordBinding(): { bindingKey: 'KeyH' | 'KeyK'; fallbackUsed: boolean; @@ -113,69 +96,6 @@ export function createKeyboardHandlers( }); } - function handleInvisiblePositionEditKeydown(e: KeyboardEvent): boolean { - if (!ctx.platform.isInvisibleLayer) return false; - - if (isInvisiblePositionToggleShortcut(e)) { - e.preventDefault(); - if (ctx.state.invisiblePositionEditMode) { - options.cancelInvisiblePositionEdit(); - } else { - options.setInvisiblePositionEditMode(true); - } - return true; - } - - if (!ctx.state.invisiblePositionEditMode) return false; - - const step = e.shiftKey - ? ctx.platform.invisiblePositionStepFastPx - : ctx.platform.invisiblePositionStepPx; - - if (e.key === 'Escape') { - e.preventDefault(); - options.cancelInvisiblePositionEdit(); - return true; - } - - if (e.key === 'Enter' || ((e.ctrlKey || e.metaKey) && e.code === 'KeyS')) { - e.preventDefault(); - options.saveInvisiblePositionEdit(); - return true; - } - - if ( - e.key === 'ArrowUp' || - e.key === 'ArrowDown' || - e.key === 'ArrowLeft' || - e.key === 'ArrowRight' || - e.key === 'h' || - e.key === 'j' || - e.key === 'k' || - e.key === 'l' || - e.key === 'H' || - e.key === 'J' || - e.key === 'K' || - e.key === 'L' - ) { - e.preventDefault(); - if (e.key === 'ArrowUp' || e.key === 'k' || e.key === 'K') { - ctx.state.invisibleSubtitleOffsetYPx += step; - } else if (e.key === 'ArrowDown' || e.key === 'j' || e.key === 'J') { - ctx.state.invisibleSubtitleOffsetYPx -= step; - } else if (e.key === 'ArrowLeft' || e.key === 'h' || e.key === 'H') { - ctx.state.invisibleSubtitleOffsetXPx -= step; - } else if (e.key === 'ArrowRight' || e.key === 'l' || e.key === 'L') { - ctx.state.invisibleSubtitleOffsetXPx += step; - } - options.applyInvisibleSubtitleOffsetPosition(); - options.updateInvisiblePositionEditHud(); - return true; - } - - return true; - } - function resetChord(): void { ctx.state.chordPending = false; if (ctx.state.chordTimeout !== null) { @@ -188,9 +108,7 @@ export function createKeyboardHandlers( updateKeybindings(await window.electronAPI.getKeybindings()); document.addEventListener('keydown', (e: KeyboardEvent) => { - const yomitanPopup = document.querySelector('iframe[id^="yomitan-popup"]'); - if (yomitanPopup) return; - if (handleInvisiblePositionEditKeydown(e)) return; + if (hasYomitanPopupIframe(document)) return; if (ctx.state.runtimeOptionsModalOpen) { options.handleRuntimeOptionsKeydown(e); diff --git a/src/renderer/handlers/mouse.ts b/src/renderer/handlers/mouse.ts index e5da966..5e94dc9 100644 --- a/src/renderer/handlers/mouse.ts +++ b/src/renderer/handlers/mouse.ts @@ -1,20 +1,50 @@ import type { ModalStateReader, RendererContext } from '../context'; +import { + YOMITAN_POPUP_HIDDEN_EVENT, + YOMITAN_POPUP_SHOWN_EVENT, + hasYomitanPopupIframe, + isYomitanPopupIframe, +} from '../yomitan-popup.js'; export function createMouseHandlers( ctx: RendererContext, options: { modalStateReader: ModalStateReader; - applyInvisibleSubtitleLayoutFromMpvMetrics: (metrics: any, source: string) => void; applyYPercent: (yPercent: number) => void; getCurrentYPercent: () => number; persistSubtitlePositionPatch: (patch: { yPercent: number }) => void; - reportHoveredTokenIndex: (tokenIndex: number | null) => void; }, ) { - const wordSegmenter = - typeof Intl !== 'undefined' && 'Segmenter' in Intl - ? new Intl.Segmenter(undefined, { granularity: 'word' }) - : null; + let yomitanPopupVisible = false; + + function enablePopupInteraction(): void { + yomitanPopupVisible = true; + ctx.dom.overlay.classList.add('interactive'); + if (ctx.platform.shouldToggleMouseIgnore) { + window.electronAPI.setIgnoreMouseEvents(false); + } + if (ctx.platform.isMacOSPlatform) { + window.focus(); + } + } + + function disablePopupInteractionIfIdle(): void { + if (hasYomitanPopupIframe(document)) { + yomitanPopupVisible = true; + return; + } + + yomitanPopupVisible = false; + if ( + !ctx.state.isOverSubtitle && + !options.modalStateReader.isAnyModalOpen() + ) { + ctx.dom.overlay.classList.remove('interactive'); + if (ctx.platform.shouldToggleMouseIgnore) { + window.electronAPI.setIgnoreMouseEvents(true, { forward: true }); + } + } + } function handleMouseEnter(): void { ctx.state.isOverSubtitle = true; @@ -26,17 +56,8 @@ export function createMouseHandlers( function handleMouseLeave(): void { ctx.state.isOverSubtitle = false; - const yomitanPopup = document.querySelector('iframe[id^="yomitan-popup"]'); - if ( - !yomitanPopup && - !options.modalStateReader.isAnyModalOpen() && - !ctx.state.invisiblePositionEditMode - ) { - ctx.dom.overlay.classList.remove('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(true, { forward: true }); - } - } + if (yomitanPopupVisible) return; + disablePopupInteractionIfIdle(); } function setupDragging(): void { @@ -75,238 +96,8 @@ export function createMouseHandlers( }); } - function getCaretTextPointRange(clientX: number, clientY: number): Range | null { - const documentWithCaretApi = document as Document & { - caretRangeFromPoint?: (x: number, y: number) => Range | null; - caretPositionFromPoint?: ( - x: number, - y: number, - ) => { offsetNode: Node; offset: number } | null; - }; - - if (typeof documentWithCaretApi.caretRangeFromPoint === 'function') { - return documentWithCaretApi.caretRangeFromPoint(clientX, clientY); - } - - if (typeof documentWithCaretApi.caretPositionFromPoint === 'function') { - const caretPosition = documentWithCaretApi.caretPositionFromPoint(clientX, clientY); - if (!caretPosition) return null; - const range = document.createRange(); - range.setStart(caretPosition.offsetNode, caretPosition.offset); - range.collapse(true); - return range; - } - - return null; - } - - function getTextOffsetWithinSubtitleRoot(targetNode: Text, targetOffset: number): number | null { - const clampedTargetOffset = Math.max(0, Math.min(targetOffset, targetNode.data.length)); - const walker = document.createTreeWalker(ctx.dom.subtitleRoot, NodeFilter.SHOW_ALL); - let totalOffset = 0; - - let node: Node | null = walker.currentNode; - while (node) { - if (node.nodeType === Node.TEXT_NODE) { - const textNode = node as Text; - if (textNode === targetNode) { - return totalOffset + clampedTargetOffset; - } - totalOffset += textNode.data.length; - } else if ( - node.nodeType === Node.ELEMENT_NODE && - (node as Element).tagName.toUpperCase() === 'BR' - ) { - totalOffset += 1; - } - node = walker.nextNode(); - } - - return null; - } - - function resolveHoveredInvisibleTokenIndex(event: MouseEvent): number | null { - if (!(event.target instanceof Node)) { - return null; - } - if (!ctx.dom.subtitleRoot.contains(event.target)) { - return null; - } - if (ctx.state.invisibleTokenHoverRanges.length === 0) { - return null; - } - - const caretRange = getCaretTextPointRange(event.clientX, event.clientY); - if (!caretRange) { - return null; - } - if (caretRange.startContainer.nodeType !== Node.TEXT_NODE) { - return null; - } - if (!ctx.dom.subtitleRoot.contains(caretRange.startContainer)) { - return null; - } - - const textOffset = getTextOffsetWithinSubtitleRoot( - caretRange.startContainer as Text, - caretRange.startOffset, - ); - if (textOffset === null) { - return null; - } - - for (const range of ctx.state.invisibleTokenHoverRanges) { - if (textOffset >= range.start && textOffset < range.end) { - return range.tokenIndex; - } - } - - return null; - } - - function getWordBoundsAtOffset( - text: string, - offset: number, - ): { start: number; end: number } | null { - if (!text || text.length === 0) return null; - - const clampedOffset = Math.max(0, Math.min(offset, text.length)); - const probeIndex = clampedOffset >= text.length ? Math.max(0, text.length - 1) : clampedOffset; - - if (wordSegmenter) { - for (const part of wordSegmenter.segment(text)) { - const start = part.index; - const end = start + part.segment.length; - if (probeIndex >= start && probeIndex < end) { - if (part.isWordLike === false) return null; - return { start, end }; - } - } - } - - const isBoundary = (char: string): boolean => - /[\s\u3000.,!?;:()[\]{}"'`~<>/\\|@#$%^&*+=\-、。・「」『』【】〈〉《》]/.test(char); - - const probeChar = text[probeIndex]; - if (!probeChar || isBoundary(probeChar)) return null; - - let start = probeIndex; - while (start > 0 && !isBoundary(text[start - 1] ?? '')) { - start -= 1; - } - - let end = probeIndex + 1; - while (end < text.length && !isBoundary(text[end] ?? '')) { - end += 1; - } - - if (end <= start) return null; - return { start, end }; - } - - function updateHoverWordSelection(event: MouseEvent): void { - if (!ctx.platform.isInvisibleLayer || !ctx.platform.isMacOSPlatform) return; - if (event.buttons !== 0) return; - if (!(event.target instanceof Node)) return; - if (!ctx.dom.subtitleRoot.contains(event.target)) return; - - const caretRange = getCaretTextPointRange(event.clientX, event.clientY); - if (!caretRange) return; - if (caretRange.startContainer.nodeType !== Node.TEXT_NODE) return; - if (!ctx.dom.subtitleRoot.contains(caretRange.startContainer)) return; - - const textNode = caretRange.startContainer as Text; - const wordBounds = getWordBoundsAtOffset(textNode.data, caretRange.startOffset); - if (!wordBounds) return; - - const selectionKey = `${wordBounds.start}:${wordBounds.end}:${textNode.data.slice( - wordBounds.start, - wordBounds.end, - )}`; - if ( - selectionKey === ctx.state.lastHoverSelectionKey && - textNode === ctx.state.lastHoverSelectionNode - ) { - return; - } - - const selection = window.getSelection(); - if (!selection) return; - - const range = document.createRange(); - range.setStart(textNode, wordBounds.start); - range.setEnd(textNode, wordBounds.end); - - selection.removeAllRanges(); - selection.addRange(range); - ctx.state.lastHoverSelectionKey = selectionKey; - ctx.state.lastHoverSelectionNode = textNode; - } - - function setupInvisibleHoverSelection(): void { - if (!ctx.platform.isInvisibleLayer || !ctx.platform.isMacOSPlatform) return; - - ctx.dom.subtitleRoot.addEventListener('mousemove', (event: MouseEvent) => { - updateHoverWordSelection(event); - }); - - ctx.dom.subtitleRoot.addEventListener('mouseleave', () => { - ctx.state.lastHoverSelectionKey = ''; - ctx.state.lastHoverSelectionNode = null; - }); - } - - function setupInvisibleTokenHoverReporter(): void { - if (!ctx.platform.isInvisibleLayer) return; - - let pendingNullHoverTimer: ReturnType | null = null; - const clearPendingNullHoverTimer = (): void => { - if (pendingNullHoverTimer !== null) { - clearTimeout(pendingNullHoverTimer); - pendingNullHoverTimer = null; - } - }; - - const reportHoveredToken = (tokenIndex: number | null): void => { - if (ctx.state.lastHoveredTokenIndex === tokenIndex) return; - ctx.state.lastHoveredTokenIndex = tokenIndex; - options.reportHoveredTokenIndex(tokenIndex); - }; - - const queueNullHoveredToken = (): void => { - if (pendingNullHoverTimer !== null) return; - pendingNullHoverTimer = setTimeout(() => { - pendingNullHoverTimer = null; - reportHoveredToken(null); - }, 120); - }; - - ctx.dom.subtitleRoot.addEventListener('mousemove', (event: MouseEvent) => { - const tokenIndex = resolveHoveredInvisibleTokenIndex(event); - if (tokenIndex === null) { - queueNullHoveredToken(); - return; - } - clearPendingNullHoverTimer(); - reportHoveredToken(tokenIndex); - }); - - ctx.dom.subtitleRoot.addEventListener('mouseleave', () => { - clearPendingNullHoverTimer(); - reportHoveredToken(null); - }); - } - function setupResizeHandler(): void { window.addEventListener('resize', () => { - if (ctx.platform.isInvisibleLayer) { - if (!ctx.state.mpvSubtitleRenderMetrics) return; - options.applyInvisibleSubtitleLayoutFromMpvMetrics( - ctx.state.mpvSubtitleRenderMetrics, - 'resize', - ); - return; - } options.applyYPercent(options.getCurrentYPercent()); }); } @@ -325,39 +116,31 @@ export function createMouseHandlers( } function setupYomitanObserver(): void { + yomitanPopupVisible = hasYomitanPopupIframe(document); + + window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, () => { + enablePopupInteraction(); + }); + + window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, () => { + disablePopupInteractionIfIdle(); + }); + const observer = new MutationObserver((mutations: MutationRecord[]) => { for (const mutation of mutations) { mutation.addedNodes.forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return; const element = node as Element; - if ( - element.tagName === 'IFRAME' && - element.id && - element.id.startsWith('yomitan-popup') - ) { - ctx.dom.overlay.classList.add('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(false); - } + if (isYomitanPopupIframe(element)) { + enablePopupInteraction(); } }); mutation.removedNodes.forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return; const element = node as Element; - if ( - element.tagName === 'IFRAME' && - element.id && - element.id.startsWith('yomitan-popup') - ) { - if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) { - ctx.dom.overlay.classList.remove('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(true, { - forward: true, - }); - } - } + if (isYomitanPopupIframe(element)) { + disablePopupInteractionIfIdle(); } }); } @@ -373,8 +156,6 @@ export function createMouseHandlers( handleMouseEnter, handleMouseLeave, setupDragging, - setupInvisibleHoverSelection, - setupInvisibleTokenHoverReporter, setupResizeHandler, setupSelectionObserver, setupYomitanObserver, diff --git a/src/renderer/modals/jimaku.ts b/src/renderer/modals/jimaku.ts index f105eed..de7e440 100644 --- a/src/renderer/modals/jimaku.ts +++ b/src/renderer/modals/jimaku.ts @@ -251,7 +251,6 @@ export function createJimakuModal( } function openJimakuModal(): void { - if (ctx.platform.isInvisibleLayer) return; if (ctx.state.jimakuModalOpen) return; ctx.state.jimakuModalOpen = true; diff --git a/src/renderer/modals/kiku.ts b/src/renderer/modals/kiku.ts index c8482a7..53915fc 100644 --- a/src/renderer/modals/kiku.ts +++ b/src/renderer/modals/kiku.ts @@ -66,7 +66,6 @@ export function createKikuModal( original: KikuDuplicateCardInfo; duplicate: KikuDuplicateCardInfo; }): void { - if (ctx.platform.isInvisibleLayer) return; if (ctx.state.kikuModalOpen) return; ctx.state.kikuModalOpen = true; diff --git a/src/renderer/modals/runtime-options.ts b/src/renderer/modals/runtime-options.ts index b069674..7162797 100644 --- a/src/renderer/modals/runtime-options.ts +++ b/src/renderer/modals/runtime-options.ts @@ -162,8 +162,6 @@ export function createRuntimeOptionsModal( } async function openRuntimeOptionsModal(): Promise { - if (ctx.platform.isInvisibleLayer) return; - const optionsList = await window.electronAPI.getRuntimeOptions(); updateRuntimeOptions(optionsList); diff --git a/src/renderer/modals/session-help.ts b/src/renderer/modals/session-help.ts index 3df690a..1619a1c 100644 --- a/src/renderer/modals/session-help.ts +++ b/src/renderer/modals/session-help.ts @@ -96,7 +96,6 @@ const OVERLAY_SHORTCUTS: Array<{ { key: 'openRuntimeOptions', label: 'Open runtime options' }, { key: 'openJimaku', label: 'Open jimaku' }, { key: 'toggleVisibleOverlayGlobal', label: 'Show/hide visible overlay' }, - { key: 'toggleInvisibleOverlayGlobal', label: 'Show/hide invisible overlay' }, ]; function buildOverlayShortcutSections(shortcuts: RuntimeShortcutConfig): SessionHelpSection[] { diff --git a/src/renderer/modals/subsync.ts b/src/renderer/modals/subsync.ts index 4aeb225..acefa2e 100644 --- a/src/renderer/modals/subsync.ts +++ b/src/renderer/modals/subsync.ts @@ -45,8 +45,6 @@ export function createSubsyncModal( } function openSubsyncModal(payload: SubsyncManualPayload): void { - if (ctx.platform.isInvisibleLayer) return; - ctx.state.subsyncSubmitting = false; ctx.dom.subsyncRunButton.disabled = false; ctx.state.subsyncSourceTracks = payload.sourceTracks; diff --git a/src/renderer/overlay-content-measurement.ts b/src/renderer/overlay-content-measurement.ts index 086b309..1c83f2a 100644 --- a/src/renderer/overlay-content-measurement.ts +++ b/src/renderer/overlay-content-measurement.ts @@ -3,8 +3,8 @@ import type { RendererContext } from './context'; const MEASUREMENT_DEBOUNCE_MS = 80; -function isMeasurableOverlayLayer(layer: string): layer is 'visible' | 'invisible' { - return layer === 'visible' || layer === 'invisible'; +function isMeasurableOverlayLayer(layer: string): layer is 'visible' { + return layer === 'visible'; } function round2(value: number): number { diff --git a/src/renderer/overlay-legacy-cleanup.test.ts b/src/renderer/overlay-legacy-cleanup.test.ts new file mode 100644 index 0000000..4863d89 --- /dev/null +++ b/src/renderer/overlay-legacy-cleanup.test.ts @@ -0,0 +1,34 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +function readWorkspaceFile(relativePath: string): string { + return fs.readFileSync(path.join(process.cwd(), relativePath), 'utf8'); +} + +test('keyboard chord map no longer emits legacy invisible overlay script messages', () => { + const keyboardSource = readWorkspaceFile('src/renderer/handlers/keyboard.ts'); + assert.doesNotMatch(keyboardSource, /subminer-toggle-invisible/); + assert.doesNotMatch(keyboardSource, /subminer-show-invisible/); + assert.doesNotMatch(keyboardSource, /subminer-hide-invisible/); +}); + +test('overlay layer contracts no longer advertise invisible renderer layer', () => { + const typesSource = readWorkspaceFile('src/types.ts'); + assert.doesNotMatch(typesSource, /export type OverlayLayer = 'visible' \| 'invisible'/); + assert.doesNotMatch( + typesSource, + /getOverlayLayer:\s*\(\)\s*=>\s*'visible'\s*\|\s*'invisible'\s*\|\s*'modal'\s*\|\s*null/, + ); +}); + +test('renderer stylesheet no longer contains invisible-layer selectors', () => { + const cssSource = readWorkspaceFile('src/renderer/style.css'); + assert.doesNotMatch(cssSource, /body\.layer-invisible/); +}); + +test('top-level docs avoid stale overlay-layers wording', () => { + const docsReadmeSource = readWorkspaceFile('docs/README.md'); + assert.doesNotMatch(docsReadmeSource, /overlay layers/i); +}); diff --git a/src/renderer/positioning/controller.ts b/src/renderer/positioning/controller.ts index 13dc015..7a0f844 100644 --- a/src/renderer/positioning/controller.ts +++ b/src/renderer/positioning/controller.ts @@ -1,36 +1,11 @@ -import type { ModalStateReader, RendererContext } from '../context'; +import type { RendererContext } from '../context'; import { createInMemorySubtitlePositionController, - type SubtitlePositionController, + type SubtitlePositionController } from './position-state.js'; -import { - createInvisibleOffsetController, - type InvisibleOffsetController, -} from './invisible-offset.js'; -import { - createMpvSubtitleLayoutController, - type MpvSubtitleLayoutController, -} from './invisible-layout.js'; - -type PositioningControllerOptions = { - modalStateReader: Pick; - applySubtitleFontSize: (fontSize: number) => void; -}; export function createPositioningController( ctx: RendererContext, - options: PositioningControllerOptions, -) { - const visible = createInMemorySubtitlePositionController(ctx); - const invisibleOffset = createInvisibleOffsetController(ctx, options.modalStateReader); - const invisibleLayout = createMpvSubtitleLayoutController(ctx, options.applySubtitleFontSize, { - applyInvisibleSubtitleOffsetPosition: invisibleOffset.applyInvisibleSubtitleOffsetPosition, - updateInvisiblePositionEditHud: invisibleOffset.updateInvisiblePositionEditHud, - }); - - return { - ...visible, - ...invisibleOffset, - ...invisibleLayout, - } as SubtitlePositionController & InvisibleOffsetController & MpvSubtitleLayoutController; +): SubtitlePositionController { + return createInMemorySubtitlePositionController(ctx); } diff --git a/src/renderer/positioning/invisible-layout-helpers.test.ts b/src/renderer/positioning/invisible-layout-helpers.test.ts deleted file mode 100644 index e9a60d8..0000000 --- a/src/renderer/positioning/invisible-layout-helpers.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import type { MpvSubtitleRenderMetrics } from '../../types'; -import { - applyTypography, - applyVerticalPosition, - resolveBaselineCompensationPx, -} from './invisible-layout-helpers.js'; - -const METRICS: MpvSubtitleRenderMetrics = { - subPos: 100, - subFontSize: 38, - subScale: 1, - subMarginY: 34, - subMarginX: 19, - subFont: 'sans-serif', - subSpacing: 0, - subBold: false, - subItalic: false, - subBorderSize: 2.5, - subShadowOffset: 0, - subAssOverride: 'yes', - subScaleByWindow: true, - subUseMargins: true, - osdHeight: 720, - osdDimensions: null, -}; - -type TypographyTestContext = { - dom: { - subtitleRoot: { style: CSSStyleDeclaration }; - subtitleContainer: { style: CSSStyleDeclaration }; - }; - state: { - currentInvisibleSubtitleLineCount: number; - invisibleMeasuredDescentPx: number | null; - }; - platform: { - isMacOSPlatform: boolean; - }; -}; - -function withMockedComputedLineHeight(lineHeightPx: number, callback: () => void): void { - const originalGetComputedStyle = (globalThis as { getComputedStyle?: unknown }).getComputedStyle; - Object.defineProperty(globalThis, 'getComputedStyle', { - configurable: true, - value: () => - ({ - lineHeight: `${lineHeightPx}px`, - }) as CSSStyleDeclaration, - }); - try { - callback(); - } finally { - if (typeof originalGetComputedStyle === 'function') { - Object.defineProperty(globalThis, 'getComputedStyle', { - configurable: true, - value: originalGetComputedStyle, - }); - } else { - Reflect.deleteProperty(globalThis, 'getComputedStyle'); - } - } -} - -function createStyle(initial: Record = {}): CSSStyleDeclaration { - const values: Record = { ...initial }; - const target = { - setProperty: (name: string, value: string) => { - values[name] = value; - }, - getPropertyValue: (name: string) => values[name] ?? '', - } as unknown as CSSStyleDeclaration; - - return new Proxy(target, { - get(obj, prop) { - if (typeof prop === 'string') { - if (prop in obj) return obj[prop as keyof CSSStyleDeclaration]; - return values[prop] ?? ''; - } - return obj[prop as keyof CSSStyleDeclaration]; - }, - set(_obj, prop, value) { - if (typeof prop === 'string') { - values[prop] = String(value); - return true; - } - return false; - }, - }); -} - -function createContext(options: { - isMacOSPlatform: boolean; - lineCount: number; - bottomPx?: number; - topPx?: number; -}): TypographyTestContext { - const subtitleRoot = { style: createStyle() }; - const subtitleContainer = { - style: createStyle({ - bottom: typeof options.bottomPx === 'number' ? `${options.bottomPx}px` : '', - top: typeof options.topPx === 'number' ? `${options.topPx}px` : '', - }), - }; - - return { - dom: { subtitleRoot, subtitleContainer }, - state: { - currentInvisibleSubtitleLineCount: options.lineCount, - invisibleMeasuredDescentPx: null, - }, - platform: { - isMacOSPlatform: options.isMacOSPlatform, - }, - }; -} - -test('resolveBaselineCompensationPx uses measured descent when present', () => { - const compensation = resolveBaselineCompensationPx(10, 2.5, 1); - assert.equal(compensation, 16); -}); - -test('resolveBaselineCompensationPx falls back to border and shadow compensation when descent missing', () => { - const compensation = resolveBaselineCompensationPx(null, 2.5, 1); - assert.equal(compensation, 17.5); -}); - -test('applyTypography keeps macOS default letter spacing neutral when mpv spacing is zero', () => { - const ctx = createContext({ - isMacOSPlatform: true, - lineCount: 1, - bottomPx: 120, - }); - - withMockedComputedLineHeight(34, () => { - applyTypography(ctx as never, { - metrics: { ...METRICS, subSpacing: 0 }, - pxPerScaledPixel: 1, - effectiveFontSize: 34, - }); - }); - - assert.equal(ctx.dom.subtitleRoot.style.getPropertyValue('letter-spacing'), '0px'); -}); - -test('applyTypography applies full mpv letter spacing scale on macOS', () => { - const ctx = createContext({ - isMacOSPlatform: true, - lineCount: 1, - bottomPx: 120, - }); - - withMockedComputedLineHeight(34, () => { - applyTypography(ctx as never, { - metrics: { ...METRICS, subSpacing: 1.5 }, - pxPerScaledPixel: 2, - effectiveFontSize: 34, - }); - }); - - assert.equal(ctx.dom.subtitleRoot.style.getPropertyValue('letter-spacing'), '3px'); -}); - -test('applyTypography uses macOS multiline-tuned line-height for invisible overlay', () => { - const ctx = createContext({ - isMacOSPlatform: true, - lineCount: 3, - bottomPx: 120, - }); - - withMockedComputedLineHeight(34, () => { - applyTypography(ctx as never, { - metrics: METRICS, - pxPerScaledPixel: 1, - effectiveFontSize: 34, - }); - }); - - assert.equal(ctx.dom.subtitleRoot.style.getPropertyValue('line-height'), '1.62'); -}); - -test('applyVerticalPosition uses subtitle position margin and baseline compensation', () => { - const ctx = createContext({ - isMacOSPlatform: true, - lineCount: 1, - }); - - applyVerticalPosition(ctx as never, { - metrics: { ...METRICS, subPos: 90 }, - renderAreaHeight: 720, - topInset: 0, - bottomInset: 10, - marginY: 34, - borderPx: 2.5, - shadowPx: 0, - measuredDescentPx: null, - vAlign: 0, - }); - - const bottom = parseFloat(ctx.dom.subtitleContainer.style.bottom); - assert.ok(Number.isFinite(bottom)); - assert.ok(bottom > 90 && bottom < 105); -}); - -test('applyVerticalPosition uses measured descent consistently across line counts', () => { - const single = createContext({ - isMacOSPlatform: true, - lineCount: 1, - }); - const dense = createContext({ - isMacOSPlatform: true, - lineCount: 3, - }); - - applyVerticalPosition(single as never, { - metrics: METRICS, - renderAreaHeight: 720, - topInset: 0, - bottomInset: 0, - marginY: 34, - borderPx: 2.5, - shadowPx: 0, - measuredDescentPx: 12, - vAlign: 0, - }); - applyVerticalPosition(dense as never, { - metrics: METRICS, - renderAreaHeight: 720, - topInset: 0, - bottomInset: 0, - marginY: 34, - borderPx: 2.5, - shadowPx: 0, - measuredDescentPx: 12, - vAlign: 0, - }); - - const singleBottom = parseFloat(single.dom.subtitleContainer.style.bottom); - const denseBottom = parseFloat(dense.dom.subtitleContainer.style.bottom); - assert.equal(singleBottom, denseBottom); -}); diff --git a/src/renderer/positioning/invisible-layout-helpers.ts b/src/renderer/positioning/invisible-layout-helpers.ts deleted file mode 100644 index c8d526a..0000000 --- a/src/renderer/positioning/invisible-layout-helpers.ts +++ /dev/null @@ -1,228 +0,0 @@ -import type { MpvSubtitleRenderMetrics } from '../../types'; -import type { RendererContext } from '../context'; - -const INVISIBLE_MACOS_VERTICAL_NUDGE_PX = 5; -const INVISIBLE_MACOS_LINE_HEIGHT_SINGLE = '1.08'; -const INVISIBLE_MACOS_LINE_HEIGHT_MULTI = '1.35'; -const INVISIBLE_MACOS_LINE_HEIGHT_MULTI_DENSE = '1.48'; - -let fontMetricsCanvas: HTMLCanvasElement | null = null; - -export function applyContainerBaseLayout( - ctx: RendererContext, - params: { - horizontalAvailable: number; - leftInset: number; - marginX: number; - hAlign: 0 | 1 | 2; - }, -): void { - const { horizontalAvailable, leftInset, marginX, hAlign } = params; - - ctx.dom.subtitleContainer.style.position = 'absolute'; - ctx.dom.subtitleContainer.style.maxWidth = `${horizontalAvailable}px`; - ctx.dom.subtitleContainer.style.width = `${horizontalAvailable}px`; - ctx.dom.subtitleContainer.style.padding = '0'; - ctx.dom.subtitleContainer.style.background = 'transparent'; - ctx.dom.subtitleContainer.style.marginBottom = '0'; - ctx.dom.subtitleContainer.style.pointerEvents = 'none'; - ctx.dom.subtitleContainer.style.left = `${leftInset + marginX}px`; - ctx.dom.subtitleContainer.style.right = ''; - ctx.dom.subtitleContainer.style.transform = ''; - ctx.dom.subtitleContainer.style.textAlign = ''; - - if (hAlign === 0) { - ctx.dom.subtitleContainer.style.textAlign = 'left'; - ctx.dom.subtitleRoot.style.textAlign = 'left'; - } else if (hAlign === 2) { - ctx.dom.subtitleContainer.style.textAlign = 'right'; - ctx.dom.subtitleRoot.style.textAlign = 'right'; - } else { - ctx.dom.subtitleContainer.style.textAlign = 'center'; - ctx.dom.subtitleRoot.style.textAlign = 'center'; - } - - ctx.dom.subtitleRoot.style.display = 'inline-block'; - ctx.dom.subtitleRoot.style.maxWidth = '100%'; - ctx.dom.subtitleRoot.style.pointerEvents = 'auto'; -} - -export function applyVerticalPosition( - ctx: RendererContext, - params: { - metrics: MpvSubtitleRenderMetrics; - renderAreaHeight: number; - topInset: number; - bottomInset: number; - marginY: number; - borderPx: number; - shadowPx: number; - measuredDescentPx: number | null; - vAlign: 0 | 1 | 2; - }, -): void { - const baselineCompensationPx = resolveBaselineCompensationPx( - params.measuredDescentPx, - params.borderPx, - params.shadowPx, - ); - - if (params.vAlign === 2) { - ctx.dom.subtitleContainer.style.top = `${Math.max( - 0, - params.topInset + params.marginY - baselineCompensationPx, - )}px`; - ctx.dom.subtitleContainer.style.bottom = ''; - return; - } - - if (params.vAlign === 1) { - ctx.dom.subtitleContainer.style.top = '50%'; - ctx.dom.subtitleContainer.style.bottom = ''; - ctx.dom.subtitleContainer.style.transform = 'translateY(-50%)'; - return; - } - - const subPosMargin = ((100 - params.metrics.subPos) / 100) * params.renderAreaHeight; - const effectiveMargin = Math.max(params.marginY, subPosMargin); - const bottomPx = Math.max(0, params.bottomInset + effectiveMargin + baselineCompensationPx); - - ctx.dom.subtitleContainer.style.top = ''; - ctx.dom.subtitleContainer.style.bottom = `${bottomPx}px`; -} - -export function resolveBaselineCompensationPx( - measuredDescentPx: number | null, - borderPx: number, - shadowPx: number, -): number { - const outlineCompensationPx = Math.max(0, borderPx * 2 + shadowPx); - if (typeof measuredDescentPx === 'number' && Number.isFinite(measuredDescentPx) && measuredDescentPx > 0) { - return Math.max(0, measuredDescentPx + outlineCompensationPx); - } - - return Math.max(0, (borderPx + shadowPx) * 5); -} - -function resolveFontFamily(rawFont: string): string { - const strippedFont = rawFont - .replace( - /\s+(Regular|Bold|Italic|Light|Medium|Semi\s*Bold|Extra\s*Bold|Extra\s*Light|Thin|Black|Heavy|Demi\s*Bold|Book|Condensed)\s*$/i, - '', - ) - .trim(); - - return strippedFont !== rawFont - ? `"${rawFont}", "${strippedFont}", sans-serif` - : `"${rawFont}", sans-serif`; -} - -export function resolveInvisibleLineHeight(lineCount: number, isMacOSPlatform: boolean): string { - if (!isMacOSPlatform) return 'normal'; - if (lineCount >= 3) return INVISIBLE_MACOS_LINE_HEIGHT_MULTI_DENSE; - if (lineCount >= 2) return INVISIBLE_MACOS_LINE_HEIGHT_MULTI; - return INVISIBLE_MACOS_LINE_HEIGHT_SINGLE; -} - -function resolveLetterSpacing( - spacing: number, - pxPerScaledPixel: number, -): string { - if (Math.abs(spacing) > 0.0001) { - return `${spacing * pxPerScaledPixel}px`; - } - - return '0px'; -} - -function measureFontDescentPx(ctx: RendererContext): number | null { - if (typeof document === 'undefined') return null; - const computedStyle = getComputedStyle(ctx.dom.subtitleRoot); - const font = computedStyle.font?.trim(); - if (!font) return null; - - if (!fontMetricsCanvas) { - fontMetricsCanvas = document.createElement('canvas'); - } - - const context = fontMetricsCanvas.getContext('2d'); - if (!context) return null; - - context.font = font; - const metrics = context.measureText('Hg漢あ'); - if (!Number.isFinite(metrics.actualBoundingBoxDescent) || metrics.actualBoundingBoxDescent <= 0) { - return null; - } - return metrics.actualBoundingBoxDescent; -} - -function applyComputedLineHeightCompensation( - ctx: RendererContext, - effectiveFontSize: number, -): void { - const computedLineHeight = parseFloat(getComputedStyle(ctx.dom.subtitleRoot).lineHeight); - if (!Number.isFinite(computedLineHeight) || computedLineHeight <= effectiveFontSize) { - return; - } - - const halfLeading = (computedLineHeight - effectiveFontSize) / 2; - if (halfLeading <= 0.5) return; - - const currentBottom = parseFloat(ctx.dom.subtitleContainer.style.bottom); - if (Number.isFinite(currentBottom)) { - ctx.dom.subtitleContainer.style.bottom = `${Math.max(0, currentBottom - halfLeading)}px`; - } - - const currentTop = parseFloat(ctx.dom.subtitleContainer.style.top); - if (Number.isFinite(currentTop)) { - ctx.dom.subtitleContainer.style.top = `${Math.max(0, currentTop - halfLeading)}px`; - } -} - -function applyMacOSAdjustments(ctx: RendererContext): void { - const isMacOSPlatform = ctx.platform.isMacOSPlatform; - if (!isMacOSPlatform) return; - - const currentBottom = parseFloat(ctx.dom.subtitleContainer.style.bottom); - if (!Number.isFinite(currentBottom)) return; - - ctx.dom.subtitleContainer.style.bottom = `${Math.max( - 0, - currentBottom + INVISIBLE_MACOS_VERTICAL_NUDGE_PX, - )}px`; -} - -export function applyTypography( - ctx: RendererContext, - params: { - metrics: MpvSubtitleRenderMetrics; - pxPerScaledPixel: number; - effectiveFontSize: number; - }, -): void { - const isMacOSPlatform = ctx.platform.isMacOSPlatform; - const lineCount = Math.max(1, ctx.state.currentInvisibleSubtitleLineCount); - const invisibleLineHeight = resolveInvisibleLineHeight(lineCount, isMacOSPlatform); - - ctx.dom.subtitleRoot.style.setProperty('--invisible-sub-line-height', invisibleLineHeight); - ctx.dom.subtitleRoot.style.setProperty( - 'line-height', - invisibleLineHeight, - isMacOSPlatform ? 'important' : '', - ); - ctx.dom.subtitleRoot.style.fontFamily = resolveFontFamily(params.metrics.subFont); - ctx.dom.subtitleRoot.style.setProperty( - 'letter-spacing', - resolveLetterSpacing(params.metrics.subSpacing, params.pxPerScaledPixel), - isMacOSPlatform ? 'important' : '', - ); - ctx.dom.subtitleRoot.style.fontKerning = isMacOSPlatform ? 'auto' : 'none'; - ctx.dom.subtitleRoot.style.fontWeight = params.metrics.subBold ? '700' : '400'; - ctx.dom.subtitleRoot.style.fontStyle = params.metrics.subItalic ? 'italic' : 'normal'; - ctx.dom.subtitleRoot.style.transform = ''; - ctx.dom.subtitleRoot.style.transformOrigin = ''; - ctx.state.invisibleMeasuredDescentPx = measureFontDescentPx(ctx); - - applyComputedLineHeightCompensation(ctx, params.effectiveFontSize); - applyMacOSAdjustments(ctx); -} diff --git a/src/renderer/positioning/invisible-layout-metrics.test.ts b/src/renderer/positioning/invisible-layout-metrics.test.ts deleted file mode 100644 index 10eddcd..0000000 --- a/src/renderer/positioning/invisible-layout-metrics.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { strict as assert } from 'node:assert'; -import { afterEach, test } from 'node:test'; -import type { MpvSubtitleRenderMetrics } from '../../types'; -import { - applyPlatformFontCompensation, - calculateOsdScale, - calculateSubtitleMetrics, -} from './invisible-layout-metrics'; - -const BASE_METRICS: MpvSubtitleRenderMetrics = { - subPos: 100, - subFontSize: 40, - subScale: 1, - subMarginY: 34, - subMarginX: 19, - subFont: 'sans-serif', - subSpacing: 0, - subBold: false, - subItalic: false, - subBorderSize: 2, - subShadowOffset: 0, - subAssOverride: 'yes', - subScaleByWindow: false, - subUseMargins: true, - osdHeight: 720, - osdDimensions: { - w: 1920, - h: 1080, - ml: 100, - mr: 100, - mt: 80, - mb: 60, - }, -}; - -const originalWindow = globalThis.window; - -function setWindowDimensions(width: number, height: number, devicePixelRatio: number): void { - Object.defineProperty(globalThis, 'window', { - configurable: true, - value: { - innerWidth: width, - innerHeight: height, - devicePixelRatio, - }, - }); -} - -afterEach(() => { - Object.defineProperty(globalThis, 'window', { - configurable: true, - value: originalWindow, - }); -}); - -test('calculateSubtitleMetrics uses video insets for scale-by-video even when subUseMargins is true', () => { - setWindowDimensions(1920, 1080, 1); - - const ctx = { - platform: { - isMacOSPlatform: false, - isLinuxPlatform: false, - }, - } as const; - - const result = calculateSubtitleMetrics(ctx as never, BASE_METRICS); - - const expectedPxPerScaledPixel = (1080 - 80 - 60) / 720; - assert.equal(result.pxPerScaledPixel, expectedPxPerScaledPixel); - assert.equal(result.effectiveFontSize, BASE_METRICS.subFontSize * expectedPxPerScaledPixel); -}); - -test('calculateSubtitleMetrics keeps osd insets for positioning even when subUseMargins is true', () => { - setWindowDimensions(1920, 1080, 1); - - const ctx = { - platform: { - isMacOSPlatform: false, - isLinuxPlatform: false, - }, - } as const; - - const result = calculateSubtitleMetrics(ctx as never, BASE_METRICS); - - assert.equal(result.leftInset, 100); - assert.equal(result.rightInset, 100); - assert.equal(result.topInset, 80); - assert.equal(result.bottomInset, 60); - assert.equal(result.horizontalAvailable, 1720); -}); - -test('applyPlatformFontCompensation applies calibrated macOS factor', () => { - assert.equal(applyPlatformFontCompensation(100, true), 82); - assert.equal(applyPlatformFontCompensation(100, false), 100); -}); - -test('calculateOsdScale snaps near-DPR macOS ratios to devicePixelRatio', () => { - const metrics = { - ...BASE_METRICS, - osdDimensions: { - w: 3024, - h: 1701, - ml: 116, - mr: 116, - mt: 28, - mb: 28, - }, - }; - - const scale = calculateOsdScale(metrics, true, 1728, 972, 2); - assert.equal(scale, 2); -}); diff --git a/src/renderer/positioning/invisible-layout-metrics.ts b/src/renderer/positioning/invisible-layout-metrics.ts deleted file mode 100644 index 118eda8..0000000 --- a/src/renderer/positioning/invisible-layout-metrics.ts +++ /dev/null @@ -1,150 +0,0 @@ -import type { MpvSubtitleRenderMetrics } from '../../types'; -import type { RendererContext } from '../context'; - -export type SubtitleAlignment = { hAlign: 0 | 1 | 2; vAlign: 0 | 1 | 2 }; - -export type SubtitleLayoutGeometry = { - renderAreaHeight: number; - renderAreaWidth: number; - leftInset: number; - rightInset: number; - topInset: number; - bottomInset: number; - horizontalAvailable: number; - marginY: number; - marginX: number; - pxPerScaledPixel: number; - effectiveFontSize: number; -}; - -export function calculateOsdScale( - metrics: MpvSubtitleRenderMetrics, - isMacOSPlatform: boolean, - viewportWidth: number, - viewportHeight: number, - devicePixelRatio: number, -): number { - const dims = metrics.osdDimensions; - - if (!isMacOSPlatform || !dims) { - return devicePixelRatio; - } - - const ratios = [dims.w / Math.max(1, viewportWidth), dims.h / Math.max(1, viewportHeight)].filter( - (value) => Number.isFinite(value) && value > 0, - ); - - const avgRatio = - ratios.length > 0 - ? ratios.reduce((sum, value) => sum + value, 0) / ratios.length - : devicePixelRatio; - - const candidates = [1, devicePixelRatio].filter((candidate, index, list) => { - if (!Number.isFinite(candidate) || candidate <= 0) return false; - return list.indexOf(candidate) === index; - }); - - const snappedScale = candidates.reduce((best, candidate) => { - const bestDistance = Math.abs(avgRatio - best); - const candidateDistance = Math.abs(avgRatio - candidate); - return candidateDistance < bestDistance ? candidate : best; - }, candidates[0] ?? 1); - - if (Math.abs(avgRatio - snappedScale) <= 0.35) { - return snappedScale; - } - - return avgRatio > 1.25 ? avgRatio : 1; -} - -export function calculateSubtitlePosition( - _metrics: MpvSubtitleRenderMetrics, - _scale: number, - alignment: number, -): SubtitleAlignment { - return { - hAlign: ((alignment - 1) % 3) as 0 | 1 | 2, - vAlign: Math.floor((alignment - 1) / 3) as 0 | 1 | 2, - }; -} - -function resolveLinePadding( - metrics: MpvSubtitleRenderMetrics, - pxPerScaledPixel: number, -): { marginY: number; marginX: number } { - return { - marginY: metrics.subMarginY * pxPerScaledPixel, - marginX: Math.max(0, metrics.subMarginX * pxPerScaledPixel), - }; -} - -export function applyPlatformFontCompensation( - fontSizePx: number, - isMacOSPlatform: boolean, -): number { - return isMacOSPlatform ? fontSizePx * 0.82 : fontSizePx; -} - -function calculateGeometry( - metrics: MpvSubtitleRenderMetrics, - osdToCssScale: number, -): Omit { - const dims = metrics.osdDimensions; - const renderAreaHeight = dims ? dims.h / osdToCssScale : window.innerHeight; - const renderAreaWidth = dims ? dims.w / osdToCssScale : window.innerWidth; - const videoLeftInset = dims ? dims.ml / osdToCssScale : 0; - const videoRightInset = dims ? dims.mr / osdToCssScale : 0; - const videoTopInset = dims ? dims.mt / osdToCssScale : 0; - const videoBottomInset = dims ? dims.mb / osdToCssScale : 0; - - // Keep layout anchored to the same drawable video region represented by osd-dimensions. - const leftInset = videoLeftInset; - const rightInset = videoRightInset; - const topInset = videoTopInset; - const bottomInset = videoBottomInset; - const horizontalAvailable = Math.max(0, renderAreaWidth - leftInset - rightInset); - - return { - renderAreaHeight, - renderAreaWidth, - leftInset, - rightInset, - topInset, - bottomInset, - horizontalAvailable, - }; -} - -export function calculateSubtitleMetrics( - ctx: RendererContext, - metrics: MpvSubtitleRenderMetrics, -): SubtitleLayoutGeometry { - const osdToCssScale = calculateOsdScale( - metrics, - ctx.platform.isMacOSPlatform, - window.innerWidth, - window.innerHeight, - window.devicePixelRatio || 1, - ); - const geometry = calculateGeometry(metrics, osdToCssScale); - const rawVideoTopInset = metrics.osdDimensions ? metrics.osdDimensions.mt / osdToCssScale : 0; - const rawVideoBottomInset = metrics.osdDimensions ? metrics.osdDimensions.mb / osdToCssScale : 0; - const videoHeight = geometry.renderAreaHeight - rawVideoTopInset - rawVideoBottomInset; - const scaleRefHeight = metrics.subScaleByWindow ? geometry.renderAreaHeight : videoHeight; - const pxPerScaledPixel = Math.max(0.1, scaleRefHeight / 720); - const computedFontSize = - metrics.subFontSize * metrics.subScale * (ctx.platform.isLinuxPlatform ? 1 : pxPerScaledPixel); - const effectiveFontSize = applyPlatformFontCompensation( - computedFontSize, - ctx.platform.isMacOSPlatform, - ); - const spacing = resolveLinePadding(metrics, pxPerScaledPixel); - - return { - ...geometry, - marginY: spacing.marginY, - marginX: spacing.marginX, - pxPerScaledPixel, - effectiveFontSize, - }; -} diff --git a/src/renderer/positioning/invisible-layout.ts b/src/renderer/positioning/invisible-layout.ts deleted file mode 100644 index 130e046..0000000 --- a/src/renderer/positioning/invisible-layout.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { MpvSubtitleRenderMetrics } from '../../types'; -import type { RendererContext } from '../context'; -import { - applyContainerBaseLayout, - applyTypography, - applyVerticalPosition, -} from './invisible-layout-helpers.js'; -import { calculateSubtitleMetrics, calculateSubtitlePosition } from './invisible-layout-metrics.js'; - -export type MpvSubtitleLayoutController = { - applyInvisibleSubtitleLayoutFromMpvMetrics: ( - metrics: MpvSubtitleRenderMetrics, - source: string, - ) => void; -}; - -export function createMpvSubtitleLayoutController( - ctx: RendererContext, - applySubtitleFontSize: (fontSize: number) => void, - options: { - applyInvisibleSubtitleOffsetPosition: () => void; - updateInvisiblePositionEditHud: () => void; - }, -): MpvSubtitleLayoutController { - function applyInvisibleSubtitleLayoutFromMpvMetrics( - metrics: MpvSubtitleRenderMetrics, - source: string, - ): void { - ctx.state.mpvSubtitleRenderMetrics = metrics; - - const geometry = calculateSubtitleMetrics(ctx, metrics); - const alignment = calculateSubtitlePosition(metrics, geometry.pxPerScaledPixel, 2); - - applySubtitleFontSize(geometry.effectiveFontSize); - const effectiveBorderSize = metrics.subBorderSize * geometry.pxPerScaledPixel; - const effectiveShadowOffset = metrics.subShadowOffset * geometry.pxPerScaledPixel; - - document.documentElement.style.setProperty('--sub-border-size', `${effectiveBorderSize}px`); - - applyContainerBaseLayout(ctx, { - horizontalAvailable: Math.max( - 0, - geometry.horizontalAvailable - Math.round(geometry.marginX * 2), - ), - leftInset: geometry.leftInset, - marginX: geometry.marginX, - hAlign: alignment.hAlign, - }); - - applyTypography(ctx, { - metrics, - pxPerScaledPixel: geometry.pxPerScaledPixel, - effectiveFontSize: geometry.effectiveFontSize, - }); - - applyVerticalPosition(ctx, { - metrics, - renderAreaHeight: geometry.renderAreaHeight, - topInset: geometry.topInset, - bottomInset: geometry.bottomInset, - marginY: geometry.marginY, - borderPx: effectiveBorderSize, - shadowPx: effectiveShadowOffset, - measuredDescentPx: ctx.state.invisibleMeasuredDescentPx, - vAlign: alignment.vAlign, - }); - - ctx.state.invisibleLayoutBaseLeftPx = parseFloat(ctx.dom.subtitleContainer.style.left) || 0; - - const parsedBottom = parseFloat(ctx.dom.subtitleContainer.style.bottom); - ctx.state.invisibleLayoutBaseBottomPx = Number.isFinite(parsedBottom) ? parsedBottom : null; - - const parsedTop = parseFloat(ctx.dom.subtitleContainer.style.top); - ctx.state.invisibleLayoutBaseTopPx = Number.isFinite(parsedTop) ? parsedTop : null; - - options.applyInvisibleSubtitleOffsetPosition(); - options.updateInvisiblePositionEditHud(); - - if (source !== 'subtitle-change') { - console.log('[invisible-overlay] Applied mpv subtitle render metrics from', source); - } - } - - return { - applyInvisibleSubtitleLayoutFromMpvMetrics, - }; -} diff --git a/src/renderer/positioning/invisible-offset.ts b/src/renderer/positioning/invisible-offset.ts deleted file mode 100644 index cff3ac4..0000000 --- a/src/renderer/positioning/invisible-offset.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { SubtitlePosition } from '../../types'; -import type { ModalStateReader, RendererContext } from '../context'; - -export type InvisibleOffsetController = { - applyInvisibleStoredSubtitlePosition: (position: SubtitlePosition | null, source: string) => void; - applyInvisibleSubtitleOffsetPosition: () => void; - updateInvisiblePositionEditHud: () => void; - setInvisiblePositionEditMode: (enabled: boolean) => void; - saveInvisiblePositionEdit: () => void; - cancelInvisiblePositionEdit: () => void; - setupInvisiblePositionEditHud: () => void; -}; - -function formatEditHudText(offsetX: number, offsetY: number): string { - return `Position Edit Ctrl/Cmd+Shift+P toggle Arrow keys move Enter/Ctrl+S save Esc cancel x:${Math.round(offsetX)} y:${Math.round(offsetY)}`; -} - -function createEditPositionText(ctx: RendererContext): string { - return formatEditHudText( - ctx.state.invisibleSubtitleOffsetXPx, - ctx.state.invisibleSubtitleOffsetYPx, - ); -} - -function applyOffsetByBasePosition(ctx: RendererContext): void { - const nextLeft = ctx.state.invisibleLayoutBaseLeftPx + ctx.state.invisibleSubtitleOffsetXPx; - ctx.dom.subtitleContainer.style.left = `${nextLeft}px`; - - if (ctx.state.invisibleLayoutBaseBottomPx !== null) { - ctx.dom.subtitleContainer.style.bottom = `${Math.max( - 0, - ctx.state.invisibleLayoutBaseBottomPx + ctx.state.invisibleSubtitleOffsetYPx, - )}px`; - ctx.dom.subtitleContainer.style.top = ''; - return; - } - - if (ctx.state.invisibleLayoutBaseTopPx !== null) { - ctx.dom.subtitleContainer.style.top = `${Math.max( - 0, - ctx.state.invisibleLayoutBaseTopPx - ctx.state.invisibleSubtitleOffsetYPx, - )}px`; - ctx.dom.subtitleContainer.style.bottom = ''; - } -} - -export function createInvisibleOffsetController( - ctx: RendererContext, - modalStateReader: Pick, -): InvisibleOffsetController { - function setInvisiblePositionEditMode(enabled: boolean): void { - if (!ctx.platform.isInvisibleLayer) return; - if (ctx.state.invisiblePositionEditMode === enabled) return; - - ctx.state.invisiblePositionEditMode = enabled; - document.body.classList.toggle('invisible-position-edit', enabled); - - if (enabled) { - ctx.state.invisiblePositionEditStartX = ctx.state.invisibleSubtitleOffsetXPx; - ctx.state.invisiblePositionEditStartY = ctx.state.invisibleSubtitleOffsetYPx; - ctx.dom.overlay.classList.add('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(false); - } - } else { - if (!ctx.state.isOverSubtitle && !modalStateReader.isAnySettingsModalOpen()) { - ctx.dom.overlay.classList.remove('interactive'); - if (ctx.platform.shouldToggleMouseIgnore) { - window.electronAPI.setIgnoreMouseEvents(true, { forward: true }); - } - } - } - - updateInvisiblePositionEditHud(); - } - - function updateInvisiblePositionEditHud(): void { - if (!ctx.state.invisiblePositionEditHud) return; - ctx.state.invisiblePositionEditHud.textContent = createEditPositionText(ctx); - } - - function applyInvisibleSubtitleOffsetPosition(): void { - applyOffsetByBasePosition(ctx); - } - - function applyInvisibleStoredSubtitlePosition( - position: SubtitlePosition | null, - source: string, - ): void { - if (position && typeof position.yPercent === 'number' && Number.isFinite(position.yPercent)) { - ctx.state.persistedSubtitlePosition = { - ...ctx.state.persistedSubtitlePosition, - yPercent: position.yPercent, - }; - } - - if (position) { - const nextX = - typeof position.invisibleOffsetXPx === 'number' && - Number.isFinite(position.invisibleOffsetXPx) - ? position.invisibleOffsetXPx - : 0; - const nextY = - typeof position.invisibleOffsetYPx === 'number' && - Number.isFinite(position.invisibleOffsetYPx) - ? position.invisibleOffsetYPx - : 0; - ctx.state.invisibleSubtitleOffsetXPx = nextX; - ctx.state.invisibleSubtitleOffsetYPx = nextY; - } else { - ctx.state.invisibleSubtitleOffsetXPx = 0; - ctx.state.invisibleSubtitleOffsetYPx = 0; - } - - applyOffsetByBasePosition(ctx); - console.log( - '[invisible-overlay] Applied subtitle offset from', - source, - `${ctx.state.invisibleSubtitleOffsetXPx}px`, - `${ctx.state.invisibleSubtitleOffsetYPx}px`, - ); - updateInvisiblePositionEditHud(); - } - - function saveInvisiblePositionEdit(): void { - const nextPosition = { - yPercent: ctx.state.persistedSubtitlePosition.yPercent, - invisibleOffsetXPx: ctx.state.invisibleSubtitleOffsetXPx, - invisibleOffsetYPx: ctx.state.invisibleSubtitleOffsetYPx, - }; - window.electronAPI.saveSubtitlePosition(nextPosition); - setInvisiblePositionEditMode(false); - } - - function cancelInvisiblePositionEdit(): void { - ctx.state.invisibleSubtitleOffsetXPx = ctx.state.invisiblePositionEditStartX; - ctx.state.invisibleSubtitleOffsetYPx = ctx.state.invisiblePositionEditStartY; - applyOffsetByBasePosition(ctx); - setInvisiblePositionEditMode(false); - } - - function setupInvisiblePositionEditHud(): void { - if (!ctx.platform.isInvisibleLayer) return; - const hud = document.createElement('div'); - hud.id = 'invisiblePositionEditHud'; - hud.className = 'invisible-position-edit-hud'; - ctx.dom.overlay.appendChild(hud); - ctx.state.invisiblePositionEditHud = hud; - updateInvisiblePositionEditHud(); - } - - return { - applyInvisibleStoredSubtitlePosition, - applyInvisibleSubtitleOffsetPosition, - updateInvisiblePositionEditHud, - setInvisiblePositionEditMode, - saveInvisiblePositionEdit, - cancelInvisiblePositionEdit, - setupInvisiblePositionEditHud, - }; -} diff --git a/src/renderer/positioning/position-state.ts b/src/renderer/positioning/position-state.ts index a3e8d64..c21ce8d 100644 --- a/src/renderer/positioning/position-state.ts +++ b/src/renderer/positioning/position-state.ts @@ -23,25 +23,12 @@ function getPersistedYPercent(ctx: RendererContext, position: SubtitlePosition | return position.yPercent; } -function getPersistedOffset( - position: SubtitlePosition | null, - key: 'invisibleOffsetXPx' | 'invisibleOffsetYPx', -): number { - if (position && typeof position[key] === 'number' && Number.isFinite(position[key])) { - return position[key]; - } - - return 0; -} - function updatePersistedSubtitlePosition( ctx: RendererContext, position: SubtitlePosition | null, ): void { ctx.state.persistedSubtitlePosition = { yPercent: getPersistedYPercent(ctx, position), - invisibleOffsetXPx: getPersistedOffset(position, 'invisibleOffsetXPx'), - invisibleOffsetYPx: getPersistedOffset(position, 'invisibleOffsetYPx'), }; } @@ -54,14 +41,6 @@ function getNextPersistedPosition( typeof patch.yPercent === 'number' && Number.isFinite(patch.yPercent) ? patch.yPercent : ctx.state.persistedSubtitlePosition.yPercent, - invisibleOffsetXPx: - typeof patch.invisibleOffsetXPx === 'number' && Number.isFinite(patch.invisibleOffsetXPx) - ? patch.invisibleOffsetXPx - : (ctx.state.persistedSubtitlePosition.invisibleOffsetXPx ?? 0), - invisibleOffsetYPx: - typeof patch.invisibleOffsetYPx === 'number' && Number.isFinite(patch.invisibleOffsetYPx) - ? patch.invisibleOffsetYPx - : (ctx.state.persistedSubtitlePosition.invisibleOffsetYPx ?? 0), }; } diff --git a/src/renderer/state.ts b/src/renderer/state.ts index 860877a..9bde4fe 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -3,7 +3,6 @@ import type { JimakuFileEntry, KikuDuplicateCardInfo, KikuFieldGroupingChoice, - MpvSubtitleRenderMetrics, RuntimeOptionId, RuntimeOptionState, RuntimeOptionValue, @@ -57,29 +56,6 @@ export type RendererState = { sessionHelpModalOpen: boolean; sessionHelpSelectedIndex: number; - mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics | null; - invisiblePositionEditMode: boolean; - invisiblePositionEditStartX: number; - invisiblePositionEditStartY: number; - invisibleSubtitleOffsetXPx: number; - invisibleSubtitleOffsetYPx: number; - invisibleLayoutBaseLeftPx: number; - invisibleLayoutBaseBottomPx: number | null; - invisibleLayoutBaseTopPx: number | null; - invisiblePositionEditHud: HTMLDivElement | null; - currentInvisibleSubtitleLineCount: number; - - lastHoverSelectionKey: string; - lastHoverSelectionNode: Text | null; - lastHoveredTokenIndex: number | null; - invisibleTokenHoverSourceText: string; - invisibleTokenHoverRanges: Array<{ - start: number; - end: number; - tokenIndex: number; - }>; - invisibleMeasuredDescentPx: number | null; - knownWordColor: string; nPlusOneColor: string; jlptN1Color: string; @@ -142,25 +118,6 @@ export function createRendererState(): RendererState { sessionHelpModalOpen: false, sessionHelpSelectedIndex: 0, - mpvSubtitleRenderMetrics: null, - invisiblePositionEditMode: false, - invisiblePositionEditStartX: 0, - invisiblePositionEditStartY: 0, - invisibleSubtitleOffsetXPx: 0, - invisibleSubtitleOffsetYPx: 0, - invisibleLayoutBaseLeftPx: 0, - invisibleLayoutBaseBottomPx: null, - invisibleLayoutBaseTopPx: null, - invisiblePositionEditHud: null, - currentInvisibleSubtitleLineCount: 1, - - lastHoverSelectionKey: '', - lastHoverSelectionNode: null, - lastHoveredTokenIndex: null, - invisibleTokenHoverSourceText: '', - invisibleTokenHoverRanges: [], - invisibleMeasuredDescentPx: null, - knownWordColor: '#a6da95', nPlusOneColor: '#c6a0f6', jlptN1Color: '#ed8796', diff --git a/src/renderer/yomitan-popup.ts b/src/renderer/yomitan-popup.ts new file mode 100644 index 0000000..0950dde --- /dev/null +++ b/src/renderer/yomitan-popup.ts @@ -0,0 +1,16 @@ +export const YOMITAN_POPUP_IFRAME_SELECTOR = 'iframe.yomitan-popup, iframe[id^="yomitan-popup"]'; +export const YOMITAN_POPUP_SHOWN_EVENT = 'yomitan-popup-shown'; +export const YOMITAN_POPUP_HIDDEN_EVENT = 'yomitan-popup-hidden'; + +export function isYomitanPopupIframe(element: Element | null): boolean { + if (!element) return false; + if (element.tagName.toUpperCase() !== 'IFRAME') return false; + + const hasModernPopupClass = element.classList?.contains('yomitan-popup') ?? false; + const hasLegacyPopupId = (element.id ?? '').startsWith('yomitan-popup'); + return hasModernPopupClass || hasLegacyPopupId; +} + +export function hasYomitanPopupIframe(root: ParentNode = document): boolean { + return root.querySelector(YOMITAN_POPUP_IFRAME_SELECTOR) !== null; +} diff --git a/src/shared/ipc/validators.ts b/src/shared/ipc/validators.ts index 1ee1984..babd923 100644 --- a/src/shared/ipc/validators.ts +++ b/src/shared/ipc/validators.ts @@ -40,18 +40,8 @@ export function parseSubtitlePosition(value: unknown): SubtitlePosition | null { if (!isObject(value) || !isFiniteNumber(value.yPercent)) { return null; } - const hasX = value.invisibleOffsetXPx !== undefined; - if (hasX && !isFiniteNumber(value.invisibleOffsetXPx)) { - return null; - } - const hasY = value.invisibleOffsetYPx !== undefined; - if (hasY && !isFiniteNumber(value.invisibleOffsetYPx)) { - return null; - } return { yPercent: value.yPercent, - invisibleOffsetXPx: hasX ? (value.invisibleOffsetXPx as number) : undefined, - invisibleOffsetYPx: hasY ? (value.invisibleOffsetYPx as number) : undefined, }; }