refactor: remove invisible subtitle overlay code

This commit is contained in:
2026-02-26 16:40:46 -08:00
parent 643f8eb958
commit 74554a30f0
119 changed files with 691 additions and 2946 deletions

View File

@@ -73,7 +73,7 @@ subminer app --start --yomitan
```bash ```bash
subminer app --start --background 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 ## Requirements

View File

@@ -16,7 +16,7 @@ make docs-preview # Preview built site at http://localhost:4173
- [Installation](/installation) — Requirements, Linux/macOS/Windows install, mpv plugin setup - [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 - [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 ### Reference

View File

@@ -74,7 +74,7 @@ The configuration file includes several main sections:
- [**Auto-Start Overlay**](#auto-start-overlay) - Automatically show overlay on MPV connection - [**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 - [**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` - [**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 - [**Jimaku**](#jimaku) - Jimaku API configuration and defaults
- [**AniList**](#anilist) - Optional post-watch progress updates - [**AniList**](#anilist) - Optional post-watch progress updates
- [**Jellyfin**](#jellyfin) - Optional Jellyfin auth, library listing, and playback launch - [**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`) | | `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 ### 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`. Default trigger is `Ctrl+Alt+S` via `shortcuts.triggerSubsync`.
Customize it there, or set it to `null` to disable. 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. Subtitle positioning can be adjusted directly in the overlay:
- `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:
- `Ctrl/Cmd+Shift+P` toggles position edit mode. - `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. - Press `Enter` or `Ctrl/Cmd+S` to save, or `Esc` to cancel.
- This edit-mode shortcut is fixed (not currently configurable in `shortcuts`/`keybindings`). - 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": { "shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+O", "toggleVisibleOverlayGlobal": "Alt+Shift+O",
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
"copySubtitle": "CommandOrControl+C", "copySubtitle": "CommandOrControl+C",
"copySubtitleMultiple": "CommandOrControl+Shift+C", "copySubtitleMultiple": "CommandOrControl+Shift+C",
"updateLastCardFromClipboard": "CommandOrControl+V", "updateLastCardFromClipboard": "CommandOrControl+V",
@@ -689,7 +680,6 @@ See `config.example.jsonc` for detailed configuration options.
| Option | Values | Description | | Option | Values | Description |
| ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `toggleVisibleOverlayGlobal` | string \| `null` | Global accelerator for toggling visible subtitle overlay (default: `"Alt+Shift+O"`) | | `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"`) | | `copySubtitle` | string \| `null` | Accelerator for copying current subtitle (default: `"CommandOrControl+C"`) |
| `copySubtitleMultiple` | string \| `null` | Accelerator for multi-copy mode (default: `"CommandOrControl+Shift+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"`) | | `updateLastCardFromClipboard` | string \| `null` | Accelerator for updating card from clipboard (default: `"CommandOrControl+V"`) |

View File

@@ -181,9 +181,6 @@ All keybindings use a `y` chord prefix — press `y`, then the second key:
| `y-s` | Start overlay | | `y-s` | Start overlay |
| `y-S` | Stop overlay | | `y-S` | Stop overlay |
| `y-t` | Toggle visible 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-o` | Open Yomitan settings |
| `y-r` | Restart overlay | | `y-r` | Restart overlay |
| `y-c` | Check overlay status | | `y-c` | Check overlay status |

View File

@@ -33,9 +33,6 @@ All keybindings use a `y` chord prefix — press `y`, then the second key:
| `y-s` | Start overlay | | `y-s` | Start overlay |
| `y-S` | Stop overlay | | `y-S` | Stop overlay |
| `y-t` | Toggle visible 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-o` | Open settings window |
| `y-r` | Restart overlay | | `y-r` | Restart overlay |
| `y-c` | Check status | | `y-c` | Check status |
@@ -50,10 +47,9 @@ SubMiner:
1. Start overlay 1. Start overlay
2. Stop overlay 2. Stop overlay
3. Toggle overlay 3. Toggle overlay
4. Toggle invisible overlay 4. Open options
5. Open options 5. Restart overlay
6. Restart overlay 6. Check status
7. Check status
``` ```
Select an item by pressing its number. Select an item by pressing its number.
@@ -84,10 +80,6 @@ auto_start=no
# Show the visible overlay on auto-start. # Show the visible overlay on auto-start.
auto_start_visible_overlay=no 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. # Show OSD messages for overlay status changes.
osd_messages=yes osd_messages=yes
@@ -129,7 +121,6 @@ aniskip_button_duration=3
| `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend | | `backend` | `auto` | `auto`, `hyprland`, `sway`, `x11`, `macos` | Window manager backend |
| `auto_start` | `no` | `yes` / `no` | Auto-start overlay on file load | | `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_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 | | `osd_messages` | `yes` | `yes` / `no` | Show OSD status messages |
| `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity | | `log_level` | `info` | `debug`, `info`, `warn`, `error` | Log verbosity |
| `aniskip_enabled` | `yes` | `yes` / `no` | Enable AniSkip intro detection | | `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-start
script-message subminer-stop script-message subminer-stop
script-message subminer-toggle 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-menu
script-message subminer-options script-message subminer-options
script-message subminer-restart script-message subminer-restart

View File

@@ -53,7 +53,6 @@
// ========================================== // ==========================================
"shortcuts": { "shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting. "toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
"toggleInvisibleOverlayGlobal": "Alt+Shift+I", // Toggle invisible overlay global setting.
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting. "copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting. "copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting. "updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
@@ -68,16 +67,6 @@
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting. "openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable. }, // 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) // Keybindings (MPV Commands)
// Extra keybindings that are merged with built-in defaults. // Extra keybindings that are merged with built-in defaults.
@@ -123,9 +112,11 @@
// Hot-reload: subtitle style changes apply live without restarting SubMiner. // Hot-reload: subtitle style changes apply live without restarting SubMiner.
// ========================================== // ==========================================
"subtitleStyle": { "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 "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 "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. "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. "fontSize": 35, // Font size setting.
"fontColor": "#cad3f5", // Font color setting. "fontColor": "#cad3f5", // Font color setting.

View File

@@ -9,7 +9,6 @@ These work system-wide regardless of which window has focus.
| Shortcut | Action | Configurable | | Shortcut | Action | Configurable |
| ------------- | ------------------------ | ---------------------------------------- | | ------------- | ------------------------ | ---------------------------------------- |
| `Alt+Shift+O` | Toggle visible overlay | `shortcuts.toggleVisibleOverlayGlobal` | | `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) | | `Alt+Shift+Y` | Open Yomitan settings | Fixed (not configurable) |
::: tip ::: 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+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` | | `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 | | 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` | Start overlay |
| `y-S` | Stop overlay | | `y-S` | Stop overlay |
| `y-t` | Toggle visible 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-o` | Open Yomitan settings |
| `y-r` | Restart overlay | | `y-r` | Restart overlay |
| `y-c` | Check overlay status | | `y-c` | Check overlay status |
@@ -112,7 +108,6 @@ All `shortcuts.*` keys accept [Electron accelerator strings](https://www.electro
"mineSentence": "CommandOrControl+S", "mineSentence": "CommandOrControl+S",
"copySubtitle": "CommandOrControl+C", "copySubtitle": "CommandOrControl+C",
"toggleVisibleOverlayGlobal": "Alt+Shift+O", "toggleVisibleOverlayGlobal": "Alt+Shift+O",
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
"openJimaku": null, // disabled "openJimaku": null, // disabled
}, },
} }

View File

@@ -153,7 +153,7 @@ SubMiner positions the overlay by tracking the mpv window. If tracking fails:
- Sway: Ensure `swaymsg` is available. - Sway: Ensure `swaymsg` is available.
- X11: Ensure `xdotool` and `xwininfo` are installed. - 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 ## Yomitan
@@ -217,10 +217,10 @@ Media generation has a 30-second timeout (60 seconds for animated AVIF). If your
**"Failed to register global shortcut"** **"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. - 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. - On Wayland, global shortcut registration has limitations depending on the compositor.
**Overlay keybindings not working** **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 ### macOS
- **Accessibility permission**: Required for window tracking. Grant it in System Settings > Privacy & Security > Accessibility. - **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` - **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`

View File

@@ -5,7 +5,7 @@ There are two ways to use SubMiner — the `subminer` wrapper script or the mpv
| Approach | Best For | | 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`). | | **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. 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 --texthooker # Launch texthooker only (no overlay window)
SubMiner.AppImage --stop # Stop overlay SubMiner.AppImage --stop # Stop overlay
SubMiner.AppImage --start --toggle # Start MPV IPC + toggle visibility 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 --show-visible-overlay # Force show visible overlay
SubMiner.AppImage --hide-visible-overlay # Force hide 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 --dev # Enable app/dev mode only
SubMiner.AppImage --start --debug # Alias for --dev SubMiner.AppImage --start --debug # Alias for --dev
SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode
@@ -173,7 +170,6 @@ Notes:
| Keybind | Action | | Keybind | Action |
| ------------- | ------------------------ | | ------------- | ------------------------ |
| `Alt+Shift+O` | Toggle visible overlay | | `Alt+Shift+O` | Toggle visible overlay |
| `Alt+Shift+I` | Toggle invisible overlay |
| `Alt+Shift+Y` | Open Yomitan settings | | `Alt+Shift+Y` | Open Yomitan settings |
`Alt+Shift+Y` is a fixed global shortcut; it is not part of `shortcuts` config. `Alt+Shift+Y` is a fixed global shortcut; it is not part of `shortcuts` config.
@@ -195,10 +191,10 @@ Notes:
| `Ctrl+W` | Quit mpv | | `Ctrl+W` | Quit mpv |
| `Right-click` | Toggle MPV pause (outside subtitle area) | | `Right-click` | Toggle MPV pause (outside subtitle area) |
| `Right-click + drag` | Move subtitle position (on subtitle) | | `Right-click + drag` | Move subtitle position (on subtitle) |
| `Ctrl/Cmd+Shift+P` | Toggle invisible subtitle position edit mode | | `Ctrl/Cmd+Shift+P` | Toggle subtitle position edit mode |
| `Arrow keys` | Move invisible subtitles while edit mode is active | | `Arrow keys` | Move subtitles while edit mode is active |
| `Enter` / `Ctrl+S` | Save invisible subtitle position in edit mode | | `Enter` / `Ctrl+S` | Save subtitle position in edit mode |
| `Esc` | Cancel invisible subtitle position edit mode | | `Esc` | Cancel subtitle position edit mode |
| `Ctrl/Cmd+A` | Append clipboard video path to MPV playlist | | `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. These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization.

View File

@@ -26,11 +26,6 @@ auto_start=no
# Automatically show visible overlay when overlay starts # Automatically show visible overlay when overlay starts
auto_start_visible_overlay=no 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) # Legacy alias (maps to auto_start_visible_overlay)
# auto_start_overlay=no # auto_start_overlay=no
@@ -70,4 +65,3 @@ aniskip_button_duration=3
# MPV keybindings provided by plugin/subminer.lua: # MPV keybindings provided by plugin/subminer.lua:
# y-s start, y-S stop, y-t toggle visible overlay # 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

View File

@@ -24,10 +24,6 @@ local function default_socket_path()
return "/tmp/subminer-socket" return "/tmp/subminer-socket"
end end
local function is_linux()
return not is_windows() and not is_macos()
end
local function is_subminer_process_running() local function is_subminer_process_running()
local command = is_windows() and { "tasklist", "/FO", "CSV", "/NH" } or { "ps", "-A", "-o", "args=" } local command = is_windows() and { "tasklist", "/FO", "CSV", "/NH" } or { "ps", "-A", "-o", "args=" }
local result = mp.command_native({ local result = mp.command_native({
@@ -140,7 +136,6 @@ local opts = {
auto_start = true, auto_start = true,
auto_start_overlay = false, -- legacy alias, maps to auto_start_visible_overlay auto_start_overlay = false, -- legacy alias, maps to auto_start_visible_overlay
auto_start_visible_overlay = false, auto_start_visible_overlay = false,
auto_start_invisible_overlay = "platform-default", -- platform-default | visible | hidden
osd_messages = true, osd_messages = true,
log_level = "info", log_level = "info",
aniskip_enabled = true, aniskip_enabled = true,
@@ -163,7 +158,6 @@ local state = {
binary_available = false, binary_available = false,
binary_path = nil, binary_path = nil,
detected_backend = nil, detected_backend = nil,
invisible_overlay_visible = false,
hover_highlight = { hover_highlight = {
revision = -1, revision = -1,
payload = nil, payload = nil,
@@ -796,6 +790,15 @@ local function fix_ass_color(input, fallback)
return b .. g .. r return b .. g .. r
end 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) local function escape_ass_text(text)
return (text or "") return (text or "")
:gsub("\\", "\\\\") :gsub("\\", "\\\\")
@@ -858,7 +861,7 @@ local function resolve_metrics()
border = sub_border_size * window_scale, border = sub_border_size * window_scale,
shadow = sub_shadow_offset * window_scale, shadow = sub_shadow_offset * window_scale,
base_color = fix_ass_color(mp.get_property("sub-color"), DEFAULT_HOVER_BASE_COLOR), 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 end
@@ -1068,7 +1071,7 @@ local function build_hover_subtitle_content(payload)
end end
local metrics = resolve_metrics() 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) 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) return inject_hover_color_to_ass(source_ass, plain_map, hover_start, hover_end, hover_color, base_color)
end end
@@ -1443,39 +1446,13 @@ local function resolve_visible_overlay_startup()
return visible return visible
end 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 function apply_startup_overlay_preferences()
local should_show_visible = resolve_visible_overlay_startup() 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" local visible_action = should_show_visible and "show-visible-overlay" or "hide-visible-overlay"
if not run_control_command(visible_action) then if not run_control_command(visible_action) then
subminer_log("warn", "process", "Failed to apply visible startup action: " .. visible_action) subminer_log("warn", "process", "Failed to apply visible startup action: " .. visible_action)
end 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 end
local function build_texthooker_args() local function build_texthooker_args()
@@ -1646,90 +1623,6 @@ local function toggle_overlay()
end end
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() local function open_options()
if not state.binary_available then if not state.binary_available then
subminer_log("error", "binary", "SubMiner binary not found") subminer_log("error", "binary", "SubMiner binary not found")
@@ -1768,7 +1661,6 @@ local function show_menu()
"Start overlay", "Start overlay",
"Stop overlay", "Stop overlay",
"Toggle overlay", "Toggle overlay",
"Toggle invisible overlay",
"Open options", "Open options",
"Restart overlay", "Restart overlay",
"Check status", "Check status",
@@ -1778,7 +1670,6 @@ local function show_menu()
start_overlay, start_overlay,
stop_overlay, stop_overlay,
toggle_overlay, toggle_overlay,
toggle_invisible_overlay,
open_options, open_options,
restart_overlay, restart_overlay,
check_status, 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-start", start_overlay)
mp.add_key_binding("y-S", "subminer-stop", stop_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-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-y", "subminer-menu", show_menu)
mp.add_key_binding("y-o", "subminer-options", open_options) mp.add_key_binding("y-o", "subminer-options", open_options)
mp.add_key_binding("y-r", "subminer-restart", restart_overlay) 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-start", start_overlay_from_script_message)
mp.register_script_message("subminer-stop", stop_overlay) mp.register_script_message("subminer-stop", stop_overlay)
mp.register_script_message("subminer-toggle", toggle_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-menu", show_menu)
mp.register_script_message("subminer-options", open_options) mp.register_script_message("subminer-options", open_options)
mp.register_script_message("subminer-restart", restart_overlay) mp.register_script_message("subminer-restart", restart_overlay)

View File

@@ -4,14 +4,11 @@ export interface CliArgs {
stop: boolean; stop: boolean;
toggle: boolean; toggle: boolean;
toggleVisibleOverlay: boolean; toggleVisibleOverlay: boolean;
toggleInvisibleOverlay: boolean;
settings: boolean; settings: boolean;
show: boolean; show: boolean;
hide: boolean; hide: boolean;
showVisibleOverlay: boolean; showVisibleOverlay: boolean;
hideVisibleOverlay: boolean; hideVisibleOverlay: boolean;
showInvisibleOverlay: boolean;
hideInvisibleOverlay: boolean;
copySubtitle: boolean; copySubtitle: boolean;
copySubtitleMultiple: boolean; copySubtitleMultiple: boolean;
mineSentence: boolean; mineSentence: boolean;
@@ -67,14 +64,11 @@ export function parseArgs(argv: string[]): CliArgs {
stop: false, stop: false,
toggle: false, toggle: false,
toggleVisibleOverlay: false, toggleVisibleOverlay: false,
toggleInvisibleOverlay: false,
settings: false, settings: false,
show: false, show: false,
hide: false, hide: false,
showVisibleOverlay: false, showVisibleOverlay: false,
hideVisibleOverlay: false, hideVisibleOverlay: false,
showInvisibleOverlay: false,
hideInvisibleOverlay: false,
copySubtitle: false, copySubtitle: false,
copySubtitleMultiple: false, copySubtitleMultiple: false,
mineSentence: false, mineSentence: false,
@@ -122,14 +116,11 @@ export function parseArgs(argv: string[]): CliArgs {
else if (arg === '--stop') args.stop = true; else if (arg === '--stop') args.stop = true;
else if (arg === '--toggle') args.toggle = true; else if (arg === '--toggle') args.toggle = true;
else if (arg === '--toggle-visible-overlay') args.toggleVisibleOverlay = 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 === '--settings' || arg === '--yomitan') args.settings = true;
else if (arg === '--show') args.show = true; else if (arg === '--show') args.show = true;
else if (arg === '--hide') args.hide = true; else if (arg === '--hide') args.hide = true;
else if (arg === '--show-visible-overlay') args.showVisibleOverlay = true; else if (arg === '--show-visible-overlay') args.showVisibleOverlay = true;
else if (arg === '--hide-visible-overlay') args.hideVisibleOverlay = 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') args.copySubtitle = true;
else if (arg === '--copy-subtitle-multiple') args.copySubtitleMultiple = true; else if (arg === '--copy-subtitle-multiple') args.copySubtitleMultiple = true;
else if (arg === '--mine-sentence') args.mineSentence = true; else if (arg === '--mine-sentence') args.mineSentence = true;
@@ -263,14 +254,11 @@ export function hasExplicitCommand(args: CliArgs): boolean {
args.stop || args.stop ||
args.toggle || args.toggle ||
args.toggleVisibleOverlay || args.toggleVisibleOverlay ||
args.toggleInvisibleOverlay ||
args.settings || args.settings ||
args.show || args.show ||
args.hide || args.hide ||
args.showVisibleOverlay || args.showVisibleOverlay ||
args.hideVisibleOverlay || args.hideVisibleOverlay ||
args.showInvisibleOverlay ||
args.hideInvisibleOverlay ||
args.copySubtitle || args.copySubtitle ||
args.copySubtitleMultiple || args.copySubtitleMultiple ||
args.mineSentence || args.mineSentence ||
@@ -307,7 +295,6 @@ export function shouldStartApp(args: CliArgs): boolean {
args.start || args.start ||
args.toggle || args.toggle ||
args.toggleVisibleOverlay || args.toggleVisibleOverlay ||
args.toggleInvisibleOverlay ||
args.copySubtitle || args.copySubtitle ||
args.copySubtitleMultiple || args.copySubtitleMultiple ||
args.mineSentence || args.mineSentence ||
@@ -331,13 +318,10 @@ export function commandNeedsOverlayRuntime(args: CliArgs): boolean {
return ( return (
args.toggle || args.toggle ||
args.toggleVisibleOverlay || args.toggleVisibleOverlay ||
args.toggleInvisibleOverlay ||
args.show || args.show ||
args.hide || args.hide ||
args.showVisibleOverlay || args.showVisibleOverlay ||
args.hideVisibleOverlay || args.hideVisibleOverlay ||
args.showInvisibleOverlay ||
args.hideInvisibleOverlay ||
args.copySubtitle || args.copySubtitle ||
args.copySubtitleMultiple || args.copySubtitleMultiple ||
args.mineSentence || args.mineSentence ||

View File

@@ -17,11 +17,8 @@ ${B}Session${R}
${B}Overlay${R} ${B}Overlay${R}
--toggle-visible-overlay Toggle subtitle overlay --toggle-visible-overlay Toggle subtitle overlay
--toggle-invisible-overlay Toggle interactive overlay ${D}(Yomitan lookup)${R}
--show-visible-overlay Show subtitle overlay --show-visible-overlay Show subtitle overlay
--hide-visible-overlay Hide 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 --settings Open Yomitan settings window
--auto-start-overlay Auto-hide mpv subs, show overlay on connect --auto-start-overlay Auto-hide mpv subs, show overlay on connect

View File

@@ -27,7 +27,8 @@ test('loads defaults when config is missing', () => {
assert.equal(config.discordPresence.updateIntervalMs, 3_000); assert.equal(config.discordPresence.updateIntervalMs, 3_000);
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)'); assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
assert.equal(config.subtitleStyle.preserveLineBreaks, false); 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.enabled, true);
assert.equal(config.immersionTracking.dbPath, ''); assert.equal(config.immersionTracking.dbPath, '');
assert.equal(config.immersionTracking.batchSize, 25); 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', () => { test('parses anilist.enabled and warns for invalid value', () => {
const dir = makeTempDir(); const dir = makeTempDir();
fs.writeFileSync( fs.writeFileSync(
@@ -597,19 +636,15 @@ test('warns and ignores unknown top-level config keys', () => {
assert.ok(warnings.some((warning) => warning.path === 'unknownFeatureFlag')); 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(); const dir = makeTempDir();
fs.writeFileSync( fs.writeFileSync(
path.join(dir, 'config.jsonc'), path.join(dir, 'config.jsonc'),
`{ `{
"shortcuts": { "shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+U", "toggleVisibleOverlayGlobal": "Alt+Shift+U",
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
"openJimaku": "Ctrl+Alt+J" "openJimaku": "Ctrl+Alt+J"
}, },
"invisibleOverlay": {
"startupVisibility": "hidden"
},
"bind_visible_overlay_to_mpv_sub_visibility": false, "bind_visible_overlay_to_mpv_sub_visibility": false,
"youtubeSubgen": { "youtubeSubgen": {
"primarySubLanguages": ["ja", "jpn", "jp"] "primarySubLanguages": ["ja", "jpn", "jp"]
@@ -621,9 +656,7 @@ test('parses invisible overlay config and new global shortcuts', () => {
const service = new ConfigService(dir); const service = new ConfigService(dir);
const config = service.getConfig(); const config = service.getConfig();
assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, 'Alt+Shift+U'); 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.shortcuts.openJimaku, 'Ctrl+Alt+J');
assert.equal(config.invisibleOverlay.startupVisibility, 'hidden');
assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false); assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false);
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'jpn', 'jp']); assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'jpn', 'jp']);
}); });

View File

@@ -29,7 +29,6 @@ const {
subsync, subsync,
auto_start_overlay, auto_start_overlay,
bind_visible_overlay_to_mpv_sub_visibility, bind_visible_overlay_to_mpv_sub_visibility,
invisibleOverlay,
} = CORE_DEFAULT_CONFIG; } = CORE_DEFAULT_CONFIG;
const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } = const { ankiConnect, jimaku, anilist, jellyfin, discordPresence, youtubeSubgen } =
INTEGRATIONS_DEFAULT_CONFIG; INTEGRATIONS_DEFAULT_CONFIG;
@@ -54,7 +53,6 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
jellyfin, jellyfin,
discordPresence, discordPresence,
youtubeSubgen, youtubeSubgen,
invisibleOverlay,
immersionTracking, immersionTracking,
}; };

View File

@@ -12,7 +12,6 @@ export const CORE_DEFAULT_CONFIG: Pick<
| 'subsync' | 'subsync'
| 'auto_start_overlay' | 'auto_start_overlay'
| 'bind_visible_overlay_to_mpv_sub_visibility' | 'bind_visible_overlay_to_mpv_sub_visibility'
| 'invisibleOverlay'
> = { > = {
subtitlePosition: { yPercent: 10 }, subtitlePosition: { yPercent: 10 },
keybindings: [], keybindings: [],
@@ -28,7 +27,6 @@ export const CORE_DEFAULT_CONFIG: Pick<
}, },
shortcuts: { shortcuts: {
toggleVisibleOverlayGlobal: 'Alt+Shift+O', toggleVisibleOverlayGlobal: 'Alt+Shift+O',
toggleInvisibleOverlayGlobal: 'Alt+Shift+I',
copySubtitle: 'CommandOrControl+C', copySubtitle: 'CommandOrControl+C',
copySubtitleMultiple: 'CommandOrControl+Shift+C', copySubtitleMultiple: 'CommandOrControl+Shift+C',
updateLastCardFromClipboard: 'CommandOrControl+V', updateLastCardFromClipboard: 'CommandOrControl+V',
@@ -55,7 +53,4 @@ export const CORE_DEFAULT_CONFIG: Pick<
}, },
auto_start_overlay: false, auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true, bind_visible_overlay_to_mpv_sub_visibility: true,
invisibleOverlay: {
startupVisibility: 'platform-default',
},
}; };

View File

@@ -4,7 +4,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
subtitleStyle: { subtitleStyle: {
enableJlpt: false, enableJlpt: false,
preserveLineBreaks: false, preserveLineBreaks: false,
hoverTokenColor: '#c6a0f6', hoverTokenColor: '#f4dbd6',
hoverTokenBackgroundColor: '#363a4fd6',
fontFamily: 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', '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, fontSize: 35,

View File

@@ -27,6 +27,12 @@ export function buildSubtitleConfigOptionRegistry(
defaultValue: defaultConfig.subtitleStyle.hoverTokenColor, defaultValue: defaultConfig.subtitleStyle.hoverTokenColor,
description: 'Hex color used for hovered subtitle token highlight in mpv.', 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', path: 'subtitleStyle.frequencyDictionary.enabled',
kind: 'boolean', kind: 'boolean',

View File

@@ -40,15 +40,6 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
notes: ['Hot-reload: shortcut changes apply live and update the session help modal on reopen.'], notes: ['Hot-reload: shortcut changes apply live and update the session help modal on reopen.'],
key: 'shortcuts', 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)', title: 'Keybindings (MPV Commands)',
description: [ description: [

View File

@@ -100,6 +100,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt; const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks; const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor; const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
const fallbackSubtitleStyleHoverTokenBackgroundColor =
resolved.subtitleStyle.hoverTokenBackgroundColor;
resolved.subtitleStyle = { resolved.subtitleStyle = {
...resolved.subtitleStyle, ...resolved.subtitleStyle,
...(src.subtitleStyle as ResolvedConfig['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( const frequencyDictionary = isObject(
(src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary, (src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary,
) )

View File

@@ -10,14 +10,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
stop: false, stop: false,
toggle: false, toggle: false,
toggleVisibleOverlay: false, toggleVisibleOverlay: false,
toggleInvisibleOverlay: false,
settings: false, settings: false,
show: false, show: false,
hide: false, hide: false,
showVisibleOverlay: false, showVisibleOverlay: false,
hideVisibleOverlay: false, hideVisibleOverlay: false,
showInvisibleOverlay: false,
hideInvisibleOverlay: false,
copySubtitle: false, copySubtitle: false,
copySubtitleMultiple: false, copySubtitleMultiple: false,
mineSentence: false, mineSentence: false,
@@ -94,18 +91,12 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
toggleVisibleOverlay: () => { toggleVisibleOverlay: () => {
calls.push('toggleVisibleOverlay'); calls.push('toggleVisibleOverlay');
}, },
toggleInvisibleOverlay: () => {
calls.push('toggleInvisibleOverlay');
},
openYomitanSettingsDelayed: (delayMs) => { openYomitanSettingsDelayed: (delayMs) => {
calls.push(`openYomitanSettingsDelayed:${delayMs}`); calls.push(`openYomitanSettingsDelayed:${delayMs}`);
}, },
setVisibleOverlayVisible: (visible) => { setVisibleOverlayVisible: (visible) => {
calls.push(`setVisibleOverlayVisible:${visible}`); calls.push(`setVisibleOverlayVisible:${visible}`);
}, },
setInvisibleOverlayVisible: (visible) => {
calls.push(`setInvisibleOverlayVisible:${visible}`);
},
copyCurrentSubtitle: () => { copyCurrentSubtitle: () => {
calls.push('copyCurrentSubtitle'); calls.push('copyCurrentSubtitle');
}, },
@@ -339,10 +330,6 @@ test('handleCliCommand handles visibility and utility command dispatches', () =>
args: Partial<CliArgs>; args: Partial<CliArgs>;
expected: string; expected: string;
}> = [ }> = [
{
args: { toggleInvisibleOverlay: true },
expected: 'toggleInvisibleOverlay',
},
{ args: { settings: true }, expected: 'openYomitanSettingsDelayed:1000' }, { args: { settings: true }, expected: 'openYomitanSettingsDelayed:1000' },
{ {
args: { showVisibleOverlay: true }, args: { showVisibleOverlay: true },
@@ -352,14 +339,6 @@ test('handleCliCommand handles visibility and utility command dispatches', () =>
args: { hideVisibleOverlay: true }, args: { hideVisibleOverlay: true },
expected: 'setVisibleOverlayVisible:false', expected: 'setVisibleOverlayVisible:false',
}, },
{
args: { showInvisibleOverlay: true },
expected: 'setInvisibleOverlayVisible:true',
},
{
args: { hideInvisibleOverlay: true },
expected: 'setInvisibleOverlayVisible:false',
},
{ args: { copySubtitle: true }, expected: 'copyCurrentSubtitle' }, { args: { copySubtitle: true }, expected: 'copyCurrentSubtitle' },
{ {
args: { copySubtitleMultiple: true }, args: { copySubtitleMultiple: true },

View File

@@ -16,10 +16,8 @@ export interface CliCommandServiceDeps {
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
initializeOverlayRuntime: () => void; initializeOverlayRuntime: () => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
toggleInvisibleOverlay: () => void;
openYomitanSettingsDelayed: (delayMs: number) => void; openYomitanSettingsDelayed: (delayMs: number) => void;
setVisibleOverlayVisible: (visible: boolean) => void; setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
copyCurrentSubtitle: () => void; copyCurrentSubtitle: () => void;
startPendingMultiCopy: (timeoutMs: number) => void; startPendingMultiCopy: (timeoutMs: number) => void;
mineSentenceCard: () => Promise<void>; mineSentenceCard: () => Promise<void>;
@@ -93,9 +91,7 @@ interface OverlayCliRuntime {
isInitialized: () => boolean; isInitialized: () => boolean;
initialize: () => void; initialize: () => void;
toggleVisible: () => void; toggleVisible: () => void;
toggleInvisible: () => void;
setVisible: (visible: boolean) => void; setVisible: (visible: boolean) => void;
setInvisible: (visible: boolean) => void;
} }
interface MiningCliRuntime { interface MiningCliRuntime {
@@ -180,14 +176,12 @@ export function createCliCommandDepsRuntime(
isOverlayRuntimeInitialized: options.overlay.isInitialized, isOverlayRuntimeInitialized: options.overlay.isInitialized,
initializeOverlayRuntime: options.overlay.initialize, initializeOverlayRuntime: options.overlay.initialize,
toggleVisibleOverlay: options.overlay.toggleVisible, toggleVisibleOverlay: options.overlay.toggleVisible,
toggleInvisibleOverlay: options.overlay.toggleInvisible,
openYomitanSettingsDelayed: (delayMs) => { openYomitanSettingsDelayed: (delayMs) => {
options.schedule(() => { options.schedule(() => {
options.ui.openYomitanSettings(); options.ui.openYomitanSettings();
}, delayMs); }, delayMs);
}, },
setVisibleOverlayVisible: options.overlay.setVisible, setVisibleOverlayVisible: options.overlay.setVisible,
setInvisibleOverlayVisible: options.overlay.setInvisible,
copyCurrentSubtitle: options.mining.copyCurrentSubtitle, copyCurrentSubtitle: options.mining.copyCurrentSubtitle,
startPendingMultiCopy: options.mining.startPendingMultiCopy, startPendingMultiCopy: options.mining.startPendingMultiCopy,
mineSentenceCard: options.mining.mineSentenceCard, mineSentenceCard: options.mining.mineSentenceCard,
@@ -242,14 +236,11 @@ export function handleCliCommand(
args.stop || args.stop ||
args.toggle || args.toggle ||
args.toggleVisibleOverlay || args.toggleVisibleOverlay ||
args.toggleInvisibleOverlay ||
args.settings || args.settings ||
args.show || args.show ||
args.hide || args.hide ||
args.showVisibleOverlay || args.showVisibleOverlay ||
args.hideVisibleOverlay || args.hideVisibleOverlay ||
args.showInvisibleOverlay ||
args.hideInvisibleOverlay ||
args.copySubtitle || args.copySubtitle ||
args.copySubtitleMultiple || args.copySubtitleMultiple ||
args.mineSentence || args.mineSentence ||
@@ -286,10 +277,7 @@ export function handleCliCommand(
} }
const shouldStart = const shouldStart =
args.start || args.start || args.toggle || args.toggleVisibleOverlay;
args.toggle ||
args.toggleVisibleOverlay ||
args.toggleInvisibleOverlay;
const needsOverlayRuntime = commandNeedsOverlayRuntime(args); const needsOverlayRuntime = commandNeedsOverlayRuntime(args);
const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start; const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start;
@@ -325,18 +313,12 @@ export function handleCliCommand(
if (args.toggle || args.toggleVisibleOverlay) { if (args.toggle || args.toggleVisibleOverlay) {
deps.toggleVisibleOverlay(); deps.toggleVisibleOverlay();
} else if (args.toggleInvisibleOverlay) {
deps.toggleInvisibleOverlay();
} else if (args.settings) { } else if (args.settings) {
deps.openYomitanSettingsDelayed(1000); deps.openYomitanSettingsDelayed(1000);
} else if (args.show || args.showVisibleOverlay) { } else if (args.show || args.showVisibleOverlay) {
deps.setVisibleOverlayVisible(true); deps.setVisibleOverlayVisible(true);
} else if (args.hide || args.hideVisibleOverlay) { } else if (args.hide || args.hideVisibleOverlay) {
deps.setVisibleOverlayVisible(false); deps.setVisibleOverlayVisible(false);
} else if (args.showInvisibleOverlay) {
deps.setInvisibleOverlayVisible(true);
} else if (args.hideInvisibleOverlay) {
deps.setInvisibleOverlayVisible(false);
} else if (args.copySubtitle) { } else if (args.copySubtitle) {
deps.copyCurrentSubtitle(); deps.copyCurrentSubtitle();
} else if (args.copySubtitleMultiple) { } else if (args.copySubtitleMultiple) {

View File

@@ -19,11 +19,9 @@ test('createFieldGroupingOverlayRuntime sends overlay messages and sets restore
}, },
}), }),
getVisibleOverlayVisible: () => visible, getVisibleOverlayVisible: () => visible,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: (next) => { setVisibleOverlayVisible: (next) => {
visible = next; visible = next;
}, },
setInvisibleOverlayVisible: () => {},
getResolver: () => null, getResolver: () => null,
setResolver: () => {}, setResolver: () => {},
getRestoreVisibleOverlayOnModalClose: () => restore, getRestoreVisibleOverlayOnModalClose: () => restore,
@@ -44,9 +42,7 @@ test('createFieldGroupingOverlayRuntime callback cancels when send fails', async
const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({ const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({
getMainWindow: () => null, getMainWindow: () => null,
getVisibleOverlayVisible: () => false, getVisibleOverlayVisible: () => false,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {}, setVisibleOverlayVisible: () => {},
setInvisibleOverlayVisible: () => {},
getResolver: () => resolver, getResolver: () => resolver,
setResolver: (next: ((choice: KikuFieldGroupingChoice) => void) | null) => { setResolver: (next: ((choice: KikuFieldGroupingChoice) => void) | null) => {
resolver = next; resolver = next;
@@ -87,12 +83,10 @@ test('createFieldGroupingOverlayRuntime callback restores hidden visible overlay
const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({ const runtime = createFieldGroupingOverlayRuntime<'runtime-options' | 'subsync'>({
getMainWindow: () => null, getMainWindow: () => null,
getVisibleOverlayVisible: () => visible, getVisibleOverlayVisible: () => visible,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: (nextVisible) => { setVisibleOverlayVisible: (nextVisible) => {
visible = nextVisible; visible = nextVisible;
visibilityTransitions.push(nextVisible); visibilityTransitions.push(nextVisible);
}, },
setInvisibleOverlayVisible: () => {},
getResolver: () => resolver as ((choice: KikuFieldGroupingChoice) => void) | null, getResolver: () => resolver as ((choice: KikuFieldGroupingChoice) => void) | null,
setResolver: (nextResolver: ((choice: KikuFieldGroupingChoice) => void) | null) => { setResolver: (nextResolver: ((choice: KikuFieldGroupingChoice) => void) | null) => {
resolver = nextResolver; resolver = nextResolver;

View File

@@ -11,9 +11,7 @@ interface WindowLike {
export interface FieldGroupingOverlayRuntimeOptions<T extends string> { export interface FieldGroupingOverlayRuntimeOptions<T extends string> {
getMainWindow: () => WindowLike | null; getMainWindow: () => WindowLike | null;
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void; setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
getRestoreVisibleOverlayOnModalClose: () => Set<T>; getRestoreVisibleOverlayOnModalClose: () => Set<T>;
@@ -65,9 +63,7 @@ export function createFieldGroupingOverlayRuntime<T extends string>(
) => Promise<KikuFieldGroupingChoice>) => { ) => Promise<KikuFieldGroupingChoice>) => {
return createFieldGroupingCallbackRuntime({ return createFieldGroupingCallbackRuntime({
getVisibleOverlayVisible: options.getVisibleOverlayVisible, getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible, setVisibleOverlayVisible: options.setVisibleOverlayVisible,
setInvisibleOverlayVisible: options.setInvisibleOverlayVisible,
getResolver: options.getResolver, getResolver: options.getResolver,
setResolver: options.setResolver, setResolver: options.setResolver,
sendToVisibleOverlay, sendToVisibleOverlay,

View File

@@ -2,9 +2,7 @@ import { KikuFieldGroupingChoice, KikuFieldGroupingRequestData } from '../../typ
export function createFieldGroupingCallback(options: { export function createFieldGroupingCallback(options: {
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void; setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
sendRequestToVisibleOverlay: (data: KikuFieldGroupingRequestData) => boolean; sendRequestToVisibleOverlay: (data: KikuFieldGroupingRequestData) => boolean;
@@ -22,7 +20,6 @@ export function createFieldGroupingCallback(options: {
} }
const previousVisibleOverlay = options.getVisibleOverlayVisible(); const previousVisibleOverlay = options.getVisibleOverlayVisible();
const previousInvisibleOverlay = options.getInvisibleOverlayVisible();
let settled = false; let settled = false;
const finish = (choice: KikuFieldGroupingChoice): void => { const finish = (choice: KikuFieldGroupingChoice): void => {
@@ -36,9 +33,6 @@ export function createFieldGroupingCallback(options: {
if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) { if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) {
options.setVisibleOverlayVisible(false); options.setVisibleOverlayVisible(false);
} }
if (options.getInvisibleOverlayVisible() !== previousInvisibleOverlay) {
options.setInvisibleOverlayVisible(previousInvisibleOverlay);
}
}; };
options.setResolver(finish); options.setResolver(finish);

View File

@@ -36,10 +36,8 @@ function createFakeIpcRegistrar(): {
test('createIpcDepsRuntime wires AniList handlers', async () => { test('createIpcDepsRuntime wires AniList handlers', async () => {
const calls: string[] = []; const calls: string[] = [];
const deps = createIpcDepsRuntime({ const deps = createIpcDepsRuntime({
getInvisibleWindow: () => null,
getMainWindow: () => null, getMainWindow: () => null,
getVisibleOverlayVisibility: () => false, getVisibleOverlayVisibility: () => false,
getInvisibleOverlayVisibility: () => false,
onOverlayModalClosed: () => {}, onOverlayModalClosed: () => {},
openYomitanSettings: () => {}, openYomitanSettings: () => {},
quitApp: () => {}, quitApp: () => {},
@@ -47,7 +45,6 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '', getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => null,
getSubtitlePosition: () => null, getSubtitlePosition: () => null,
getSubtitleStyle: () => null, getSubtitleStyle: () => null,
saveSubtitlePosition: () => {}, saveSubtitlePosition: () => {},
@@ -64,7 +61,6 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
setRuntimeOption: () => ({ ok: true }), setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }), cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {}, reportOverlayContentBounds: () => {},
reportHoveredSubtitleToken: () => {},
getAnilistStatus: () => ({ tokenStatus: 'resolved' }), getAnilistStatus: () => ({ tokenStatus: 'resolved' }),
clearAnilistToken: () => { clearAnilistToken: () => {
calls.push('clearAnilistToken'); calls.push('clearAnilistToken');
@@ -101,20 +97,15 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
const cycles: Array<{ id: string; direction: 1 | -1 }> = []; const cycles: Array<{ id: string; direction: 1 | -1 }> = [];
registerIpcHandlers( registerIpcHandlers(
{ {
getInvisibleWindow: () => null,
isVisibleOverlayVisible: () => false,
setInvisibleIgnoreMouseEvents: () => {},
onOverlayModalClosed: () => {}, onOverlayModalClosed: () => {},
openYomitanSettings: () => {}, openYomitanSettings: () => {},
quitApp: () => {}, quitApp: () => {},
toggleDevTools: () => {}, toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false, getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {}, toggleVisibleOverlay: () => {},
getInvisibleOverlayVisibility: () => false,
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '', getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => null,
getSubtitlePosition: () => null, getSubtitlePosition: () => null,
getSubtitleStyle: () => null, getSubtitleStyle: () => null,
saveSubtitlePosition: () => {}, saveSubtitlePosition: () => {},
@@ -138,7 +129,6 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
return { ok: true }; return { ok: true };
}, },
reportOverlayContentBounds: () => {}, reportOverlayContentBounds: () => {},
reportHoveredSubtitleToken: () => {},
getAnilistStatus: () => ({}), getAnilistStatus: () => ({}),
clearAnilistToken: () => {}, clearAnilistToken: () => {},
openAnilistSetup: () => {}, openAnilistSetup: () => {},
@@ -176,25 +166,24 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () =
test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => { test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
const { registrar, handlers } = createFakeIpcRegistrar(); const { registrar, handlers } = createFakeIpcRegistrar();
const saves: unknown[] = []; const saves: unknown[] = [];
const modals: unknown[] = []; const closedModals: unknown[] = [];
const openedModals: unknown[] = [];
registerIpcHandlers( registerIpcHandlers(
{ {
getInvisibleWindow: () => null,
isVisibleOverlayVisible: () => false,
setInvisibleIgnoreMouseEvents: () => {},
onOverlayModalClosed: (modal) => { onOverlayModalClosed: (modal) => {
modals.push(modal); closedModals.push(modal);
},
onOverlayModalOpened: (modal) => {
openedModals.push(modal);
}, },
openYomitanSettings: () => {}, openYomitanSettings: () => {},
quitApp: () => {}, quitApp: () => {},
toggleDevTools: () => {}, toggleDevTools: () => {},
getVisibleOverlayVisibility: () => false, getVisibleOverlayVisibility: () => false,
toggleVisibleOverlay: () => {}, toggleVisibleOverlay: () => {},
getInvisibleOverlayVisibility: () => false,
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '', getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => null,
getSubtitlePosition: () => null, getSubtitlePosition: () => null,
getSubtitleStyle: () => null, getSubtitleStyle: () => null,
saveSubtitlePosition: (position) => { saveSubtitlePosition: (position) => {
@@ -214,7 +203,6 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => {
setRuntimeOption: () => ({ ok: true }), setRuntimeOption: () => ({ ok: true }),
cycleRuntimeOption: () => ({ ok: true }), cycleRuntimeOption: () => ({ ok: true }),
reportOverlayContentBounds: () => {}, reportOverlayContentBounds: () => {},
reportHoveredSubtitleToken: () => {},
getAnilistStatus: () => ({}), getAnilistStatus: () => ({}),
clearAnilistToken: () => {}, clearAnilistToken: () => {},
openAnilistSetup: () => {}, 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: 'bad' });
handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 }); handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 });
assert.deepEqual(saves, [ 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)!({}, 'not-a-modal');
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync');
handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'kiku'); 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']);
}); });

View File

@@ -19,20 +19,16 @@ import {
} from '../../shared/ipc/validators'; } from '../../shared/ipc/validators';
export interface IpcServiceDeps { export interface IpcServiceDeps {
getInvisibleWindow: () => WindowLike | null;
isVisibleOverlayVisible: () => boolean;
setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
onOverlayModalClosed: (modal: OverlayHostedModal) => void; onOverlayModalClosed: (modal: OverlayHostedModal) => void;
onOverlayModalOpened?: (modal: OverlayHostedModal) => void;
openYomitanSettings: () => void; openYomitanSettings: () => void;
quitApp: () => void; quitApp: () => void;
toggleDevTools: () => void; toggleDevTools: () => void;
getVisibleOverlayVisibility: () => boolean; getVisibleOverlayVisibility: () => boolean;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
getInvisibleOverlayVisibility: () => boolean;
tokenizeCurrentSubtitle: () => Promise<unknown>; tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string; getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string; getCurrentSubtitleAss: () => string;
getMpvSubtitleRenderMetrics: () => unknown;
getSubtitlePosition: () => unknown; getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown; getSubtitleStyle: () => unknown;
saveSubtitlePosition: (position: SubtitlePosition) => void; saveSubtitlePosition: (position: SubtitlePosition) => void;
@@ -54,7 +50,6 @@ export interface IpcServiceDeps {
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown; cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown;
reportOverlayContentBounds: (payload: unknown) => void; reportOverlayContentBounds: (payload: unknown) => void;
reportHoveredSubtitleToken: (tokenIndex: number | null) => void;
getAnilistStatus: () => unknown; getAnilistStatus: () => unknown;
clearAnilistToken: () => void; clearAnilistToken: () => void;
openAnilistSetup: () => void; openAnilistSetup: () => void;
@@ -91,18 +86,16 @@ interface IpcMainRegistrar {
} }
export interface IpcDepsRuntimeOptions { export interface IpcDepsRuntimeOptions {
getInvisibleWindow: () => WindowLike | null;
getMainWindow: () => WindowLike | null; getMainWindow: () => WindowLike | null;
getVisibleOverlayVisibility: () => boolean; getVisibleOverlayVisibility: () => boolean;
getInvisibleOverlayVisibility: () => boolean;
onOverlayModalClosed: (modal: OverlayHostedModal) => void; onOverlayModalClosed: (modal: OverlayHostedModal) => void;
onOverlayModalOpened?: (modal: OverlayHostedModal) => void;
openYomitanSettings: () => void; openYomitanSettings: () => void;
quitApp: () => void; quitApp: () => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
tokenizeCurrentSubtitle: () => Promise<unknown>; tokenizeCurrentSubtitle: () => Promise<unknown>;
getCurrentSubtitleRaw: () => string; getCurrentSubtitleRaw: () => string;
getCurrentSubtitleAss: () => string; getCurrentSubtitleAss: () => string;
getMpvSubtitleRenderMetrics: () => unknown;
getSubtitlePosition: () => unknown; getSubtitlePosition: () => unknown;
getSubtitleStyle: () => unknown; getSubtitleStyle: () => unknown;
saveSubtitlePosition: (position: SubtitlePosition) => void; saveSubtitlePosition: (position: SubtitlePosition) => void;
@@ -119,7 +112,6 @@ export interface IpcDepsRuntimeOptions {
setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown; setRuntimeOption: (id: RuntimeOptionId, value: RuntimeOptionValue) => unknown;
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown; cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => unknown;
reportOverlayContentBounds: (payload: unknown) => void; reportOverlayContentBounds: (payload: unknown) => void;
reportHoveredSubtitleToken: (tokenIndex: number | null) => void;
getAnilistStatus: () => unknown; getAnilistStatus: () => unknown;
clearAnilistToken: () => void; clearAnilistToken: () => void;
openAnilistSetup: () => void; openAnilistSetup: () => void;
@@ -130,14 +122,8 @@ export interface IpcDepsRuntimeOptions {
export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcServiceDeps { export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcServiceDeps {
return { 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, onOverlayModalClosed: options.onOverlayModalClosed,
onOverlayModalOpened: options.onOverlayModalOpened,
openYomitanSettings: options.openYomitanSettings, openYomitanSettings: options.openYomitanSettings,
quitApp: options.quitApp, quitApp: options.quitApp,
toggleDevTools: () => { toggleDevTools: () => {
@@ -147,11 +133,9 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
}, },
getVisibleOverlayVisibility: options.getVisibleOverlayVisibility, getVisibleOverlayVisibility: options.getVisibleOverlayVisibility,
toggleVisibleOverlay: options.toggleVisibleOverlay, toggleVisibleOverlay: options.toggleVisibleOverlay,
getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility,
tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle, tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle,
getCurrentSubtitleRaw: options.getCurrentSubtitleRaw, getCurrentSubtitleRaw: options.getCurrentSubtitleRaw,
getCurrentSubtitleAss: options.getCurrentSubtitleAss, getCurrentSubtitleAss: options.getCurrentSubtitleAss,
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
getSubtitlePosition: options.getSubtitlePosition, getSubtitlePosition: options.getSubtitlePosition,
getSubtitleStyle: options.getSubtitleStyle, getSubtitleStyle: options.getSubtitleStyle,
saveSubtitlePosition: options.saveSubtitlePosition, saveSubtitlePosition: options.saveSubtitlePosition,
@@ -182,7 +166,6 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
setRuntimeOption: options.setRuntimeOption, setRuntimeOption: options.setRuntimeOption,
cycleRuntimeOption: options.cycleRuntimeOption, cycleRuntimeOption: options.cycleRuntimeOption,
reportOverlayContentBounds: options.reportOverlayContentBounds, reportOverlayContentBounds: options.reportOverlayContentBounds,
reportHoveredSubtitleToken: options.reportHoveredSubtitleToken,
getAnilistStatus: options.getAnilistStatus, getAnilistStatus: options.getAnilistStatus,
clearAnilistToken: options.clearAnilistToken, clearAnilistToken: options.clearAnilistToken,
openAnilistSetup: options.openAnilistSetup, openAnilistSetup: options.openAnilistSetup,
@@ -200,18 +183,8 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
const parsedOptions = parseOptionalForwardingOptions(options); const parsedOptions = parseOptionalForwardingOptions(options);
const senderWindow = BrowserWindow.fromWebContents((event as IpcMainEvent).sender); const senderWindow = BrowserWindow.fromWebContents((event as IpcMainEvent).sender);
if (senderWindow && !senderWindow.isDestroyed()) { 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; if (!parsedModal) return;
deps.onOverlayModalClosed(parsedModal); 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, () => { ipc.on(IPC_CHANNELS.command.openYomitanSettings, () => {
deps.openYomitanSettings(); deps.openYomitanSettings();
@@ -245,10 +224,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return deps.getVisibleOverlayVisibility(); return deps.getVisibleOverlayVisibility();
}); });
ipc.handle(IPC_CHANNELS.request.getInvisibleOverlayVisibility, () => {
return deps.getInvisibleOverlayVisibility();
});
ipc.handle(IPC_CHANNELS.request.getCurrentSubtitle, async () => { ipc.handle(IPC_CHANNELS.request.getCurrentSubtitle, async () => {
return await deps.tokenizeCurrentSubtitle(); return await deps.tokenizeCurrentSubtitle();
}); });
@@ -261,10 +236,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
return deps.getCurrentSubtitleAss(); return deps.getCurrentSubtitleAss();
}); });
ipc.handle(IPC_CHANNELS.request.getMpvSubtitleRenderMetrics, () => {
return deps.getMpvSubtitleRenderMetrics();
});
ipc.handle(IPC_CHANNELS.request.getSubtitlePosition, () => { ipc.handle(IPC_CHANNELS.request.getSubtitlePosition, () => {
return deps.getSubtitlePosition(); return deps.getSubtitlePosition();
}); });
@@ -358,17 +329,6 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
deps.reportOverlayContentBounds(payload); 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, () => { ipc.handle(IPC_CHANNELS.request.getAnilistStatus, () => {
return deps.getAnilistStatus(); return deps.getAnilistStatus();
}); });

View File

@@ -33,13 +33,51 @@ test('sendToVisibleOverlayRuntime restores visibility flag when opening hidden o
assert.deepEqual(sent, [['runtime-options:open']]); 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 () => { test('createFieldGroupingCallbackRuntime cancels when overlay request cannot be sent', async () => {
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null; let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
const callback = createFieldGroupingCallbackRuntime<'runtime-options' | 'subsync'>({ const callback = createFieldGroupingCallbackRuntime<'runtime-options' | 'subsync'>({
getVisibleOverlayVisible: () => false, getVisibleOverlayVisible: () => false,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {}, setVisibleOverlayVisible: () => {},
setInvisibleOverlayVisible: () => {},
getResolver: () => resolver, getResolver: () => resolver,
setResolver: (next) => { setResolver: (next) => {
resolver = next; resolver = next;

View File

@@ -26,27 +26,32 @@ export function sendToVisibleOverlayRuntime<T extends string>(options: {
options.mainWindow!.webContents.send(options.channel, options.payload); 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', () => { options.mainWindow.webContents.once('did-finish-load', () => {
if ( if (!options.mainWindow || options.mainWindow.isDestroyed()) return;
options.mainWindow && if (!options.mainWindow.webContents.isLoading()) {
!options.mainWindow.isDestroyed() &&
!options.mainWindow.webContents.isLoading()
) {
sendNow(); sendNow();
} }
}); });
return true; return true;
} }
sendNow(); sendNow();
return true; return true;
} }
export function createFieldGroupingCallbackRuntime<T extends string>(options: { export function createFieldGroupingCallbackRuntime<T extends string>(options: {
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void; setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null;
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void;
sendToVisibleOverlay: ( sendToVisibleOverlay: (
@@ -57,9 +62,7 @@ export function createFieldGroupingCallbackRuntime<T extends string>(options: {
}): (data: KikuFieldGroupingRequestData) => Promise<KikuFieldGroupingChoice> { }): (data: KikuFieldGroupingRequestData) => Promise<KikuFieldGroupingChoice> {
return createFieldGroupingCallback({ return createFieldGroupingCallback({
getVisibleOverlayVisible: options.getVisibleOverlayVisible, getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisible: options.setVisibleOverlayVisible, setVisibleOverlayVisible: options.setVisibleOverlayVisible,
setInvisibleOverlayVisible: options.setInvisibleOverlayVisible,
getResolver: options.getResolver, getResolver: options.getResolver,
setResolver: options.setResolver, setResolver: options.setResolver,
sendRequestToVisibleOverlay: (data) => sendRequestToVisibleOverlay: (data) =>

View File

@@ -28,7 +28,7 @@ test('sanitizeOverlayContentMeasurement accepts valid payload with null rect', (
test('sanitizeOverlayContentMeasurement rejects invalid ranges', () => { test('sanitizeOverlayContentMeasurement rejects invalid ranges', () => {
const measurement = sanitizeOverlayContentMeasurement( const measurement = sanitizeOverlayContentMeasurement(
{ {
layer: 'invisible', layer: 'visible',
measuredAtMs: 100, measuredAtMs: 100,
viewport: { width: 0, height: 1080 }, viewport: { width: 0, height: 1080 },
contentRect: { x: 0, y: 0, width: 100, height: 20 }, contentRect: { x: 0, y: 0, width: 100, height: 20 },
@@ -39,7 +39,7 @@ test('sanitizeOverlayContentMeasurement rejects invalid ranges', () => {
assert.equal(measurement, null); 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({ const store = createOverlayContentMeasurementStore({
now: () => 1000, now: () => 1000,
warn: () => { warn: () => {
@@ -53,17 +53,9 @@ test('overlay measurement store keeps latest payload per layer', () => {
viewport: { width: 1280, height: 720 }, viewport: { width: 1280, height: 720 },
contentRect: { x: 50, y: 60, width: 400, height: 80 }, 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(visible?.layer, 'visible');
assert.equal(invisible?.layer, 'invisible');
assert.equal(store.getLatestByLayer('visible')?.contentRect?.width, 400); 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', () => { test('overlay measurement store rate-limits invalid payload warnings', () => {

View File

@@ -28,7 +28,7 @@ export function sanitizeOverlayContentMeasurement(
} | null; } | null;
}; };
if (candidate.layer !== 'visible' && candidate.layer !== 'invisible') { if (candidate.layer !== 'visible') {
return null; return null;
} }
@@ -112,7 +112,6 @@ export function createOverlayContentMeasurementStore(options?: {
const warn = options?.warn ?? ((message: string) => logger.warn(message)); const warn = options?.warn ?? ((message: string) => logger.warn(message));
const latestByLayer: OverlayMeasurementStore = { const latestByLayer: OverlayMeasurementStore = {
visible: null, visible: null,
invisible: null,
}; };
let droppedInvalid = 0; let droppedInvalid = 0;

View File

@@ -10,16 +10,11 @@ import {
export function initializeOverlayRuntime(options: { export function initializeOverlayRuntime(options: {
backendOverride: string | null; backendOverride: string | null;
getInitialInvisibleOverlayVisibility: () => boolean;
createMainWindow: () => void; createMainWindow: () => void;
createInvisibleWindow: () => void;
registerGlobalShortcuts: () => void; registerGlobalShortcuts: () => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
isInvisibleOverlayVisible: () => boolean;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
getOverlayWindows: () => BrowserWindow[]; getOverlayWindows: () => BrowserWindow[];
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
setWindowTracker: (tracker: BaseWindowTracker | null) => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void;
@@ -38,12 +33,8 @@ export function initializeOverlayRuntime(options: {
data: KikuFieldGroupingRequestData, data: KikuFieldGroupingRequestData,
) => Promise<KikuFieldGroupingChoice>; ) => Promise<KikuFieldGroupingChoice>;
getKnownWordCacheStatePath: () => string; getKnownWordCacheStatePath: () => string;
}): { }): void {
invisibleOverlayVisible: boolean;
} {
options.createMainWindow(); options.createMainWindow();
options.createInvisibleWindow();
const invisibleOverlayVisible = options.getInitialInvisibleOverlayVisibility();
options.registerGlobalShortcuts(); options.registerGlobalShortcuts();
const windowTracker = createWindowTracker(options.backendOverride, options.getMpvSocketPath()); const windowTracker = createWindowTracker(options.backendOverride, options.getMpvSocketPath());
@@ -51,17 +42,12 @@ export function initializeOverlayRuntime(options: {
if (windowTracker) { if (windowTracker) {
windowTracker.onGeometryChange = (geometry: WindowGeometry) => { windowTracker.onGeometryChange = (geometry: WindowGeometry) => {
options.updateVisibleOverlayBounds(geometry); options.updateVisibleOverlayBounds(geometry);
options.updateInvisibleOverlayBounds(geometry);
}; };
windowTracker.onWindowFound = (geometry: WindowGeometry) => { windowTracker.onWindowFound = (geometry: WindowGeometry) => {
options.updateVisibleOverlayBounds(geometry); options.updateVisibleOverlayBounds(geometry);
options.updateInvisibleOverlayBounds(geometry);
if (options.isVisibleOverlayVisible()) { if (options.isVisibleOverlayVisible()) {
options.updateVisibleOverlayVisibility(); options.updateVisibleOverlayVisibility();
} }
if (options.isInvisibleOverlayVisible()) {
options.updateInvisibleOverlayVisibility();
}
}; };
windowTracker.onWindowLost = () => { windowTracker.onWindowLost = () => {
for (const window of options.getOverlayWindows()) { for (const window of options.getOverlayWindows()) {
@@ -101,7 +87,4 @@ export function initializeOverlayRuntime(options: {
} }
options.updateVisibleOverlayVisibility(); options.updateVisibleOverlayVisibility();
options.updateInvisibleOverlayVisibility();
return { invisibleOverlayVisible };
} }

View File

@@ -10,7 +10,6 @@ import {
function makeShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): ConfiguredShortcuts { function makeShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): ConfiguredShortcuts {
return { return {
toggleVisibleOverlayGlobal: null, toggleVisibleOverlayGlobal: null,
toggleInvisibleOverlayGlobal: null,
copySubtitle: null, copySubtitle: null,
copySubtitleMultiple: null, copySubtitleMultiple: null,
updateLastCardFromClipboard: null, updateLastCardFromClipboard: null,

View File

@@ -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...']);
});

View File

@@ -1,4 +1,4 @@
import { BrowserWindow, screen } from 'electron'; import type { BrowserWindow } from 'electron';
import { BaseWindowTracker } from '../../window-trackers'; import { BaseWindowTracker } from '../../window-trackers';
import { WindowGeometry } from '../../types'; import { WindowGeometry } from '../../types';
@@ -10,14 +10,19 @@ export function updateVisibleOverlayVisibility(args: {
setTrackerNotReadyWarningShown: (shown: boolean) => void; setTrackerNotReadyWarningShown: (shown: boolean) => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
ensureOverlayWindowLevel: (window: BrowserWindow) => void; ensureOverlayWindowLevel: (window: BrowserWindow) => void;
syncPrimaryOverlayWindowLayer: (layer: 'visible') => void;
enforceOverlayLayerOrder: () => void; enforceOverlayLayerOrder: () => void;
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
isMacOSPlatform?: boolean;
showOverlayLoadingOsd?: (message: string) => void;
resolveFallbackBounds: () => WindowGeometry;
}): void { }): void {
if (!args.mainWindow || args.mainWindow.isDestroyed()) { if (!args.mainWindow || args.mainWindow.isDestroyed()) {
return; return;
} }
if (!args.visibleOverlayVisible) { if (!args.visibleOverlayVisible) {
args.setTrackerNotReadyWarningShown(false);
args.mainWindow.hide(); args.mainWindow.hide();
args.syncOverlayShortcuts(); args.syncOverlayShortcuts();
return; return;
@@ -29,6 +34,8 @@ export function updateVisibleOverlayVisibility(args: {
if (geometry) { if (geometry) {
args.updateVisibleOverlayBounds(geometry); args.updateVisibleOverlayBounds(geometry);
} }
args.syncPrimaryOverlayWindowLayer('visible');
args.mainWindow.setIgnoreMouseEvents(false);
args.ensureOverlayWindowLevel(args.mainWindow); args.ensureOverlayWindowLevel(args.mainWindow);
args.mainWindow.show(); args.mainWindow.show();
args.mainWindow.focus(); args.mainWindow.focus();
@@ -38,7 +45,18 @@ export function updateVisibleOverlayVisibility(args: {
} }
if (!args.windowTracker) { 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.setTrackerNotReadyWarningShown(false);
args.syncPrimaryOverlayWindowLayer('visible');
args.mainWindow.setIgnoreMouseEvents(false);
args.ensureOverlayWindowLevel(args.mainWindow); args.ensureOverlayWindowLevel(args.mainWindow);
args.mainWindow.show(); args.mainWindow.show();
args.mainWindow.focus(); args.mainWindow.focus();
@@ -49,16 +67,21 @@ export function updateVisibleOverlayVisibility(args: {
if (!args.trackerNotReadyWarningShown) { if (!args.trackerNotReadyWarningShown) {
args.setTrackerNotReadyWarningShown(true); args.setTrackerNotReadyWarningShown(true);
if (args.isMacOSPlatform) {
args.showOverlayLoadingOsd?.('Overlay loading...');
} }
const cursorPoint = screen.getCursorScreenPoint(); }
const display = screen.getDisplayNearestPoint(cursorPoint);
const fallbackBounds = display.workArea; if (args.isMacOSPlatform) {
args.updateVisibleOverlayBounds({ args.mainWindow.hide();
x: fallbackBounds.x, args.syncOverlayShortcuts();
y: fallbackBounds.y, return;
width: fallbackBounds.width, }
height: fallbackBounds.height,
}); const fallbackBounds = args.resolveFallbackBounds();
args.updateVisibleOverlayBounds(fallbackBounds);
args.syncPrimaryOverlayWindowLayer('visible');
args.mainWindow.setIgnoreMouseEvents(false);
args.ensureOverlayWindowLevel(args.mainWindow); args.ensureOverlayWindowLevel(args.mainWindow);
args.mainWindow.show(); args.mainWindow.show();
args.mainWindow.focus(); args.mainWindow.focus();
@@ -66,111 +89,11 @@ export function updateVisibleOverlayVisibility(args: {
args.syncOverlayShortcuts(); 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: { export function setVisibleOverlayVisible(options: {
visible: boolean; visible: boolean;
setVisibleOverlayVisibleState: (visible: boolean) => void; setVisibleOverlayVisibleState: (visible: boolean) => void;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isMpvConnected: () => boolean;
setMpvSubVisibility: (visible: boolean) => void;
}): void { }): void {
options.setVisibleOverlayVisibleState(options.visible); options.setVisibleOverlayVisibleState(options.visible);
options.updateVisibleOverlayVisibility(); 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();
} }

View File

@@ -1,7 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
getInitialInvisibleOverlayVisibility,
isAutoUpdateEnabledRuntime, isAutoUpdateEnabledRuntime,
shouldAutoInitializeOverlayRuntimeFromConfig, shouldAutoInitializeOverlayRuntimeFromConfig,
shouldBindVisibleOverlayToMpvSubVisibility, shouldBindVisibleOverlayToMpvSubVisibility,
@@ -10,9 +9,6 @@ import {
const BASE_CONFIG = { const BASE_CONFIG = {
auto_start_overlay: false, auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true, bind_visible_overlay_to_mpv_sub_visibility: true,
invisibleOverlay: {
startupVisibility: 'platform-default' as const,
},
ankiConnect: { ankiConnect: {
behavior: { behavior: {
autoUpdateNewCards: true, autoUpdateNewCards: true,
@@ -20,26 +16,7 @@ const BASE_CONFIG = {
}, },
}; };
test('getInitialInvisibleOverlayVisibility handles visibility + platform', () => { test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start', () => {
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', () => {
assert.equal(shouldAutoInitializeOverlayRuntimeFromConfig(BASE_CONFIG), false); assert.equal(shouldAutoInitializeOverlayRuntimeFromConfig(BASE_CONFIG), false);
assert.equal( assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfig({ shouldAutoInitializeOverlayRuntimeFromConfig({
@@ -48,13 +25,6 @@ test('shouldAutoInitializeOverlayRuntimeFromConfig respects auto start and visib
}), }),
true, true,
); );
assert.equal(
shouldAutoInitializeOverlayRuntimeFromConfig({
...BASE_CONFIG,
invisibleOverlay: { startupVisibility: 'visible' },
}),
true,
);
}); });
test('shouldBindVisibleOverlayToMpvSubVisibility returns config value', () => { test('shouldBindVisibleOverlayToMpvSubVisibility returns config value', () => {

View File

@@ -5,14 +5,12 @@ const logger = createLogger('main:shortcut');
export interface GlobalShortcutConfig { export interface GlobalShortcutConfig {
toggleVisibleOverlayGlobal: string | null | undefined; toggleVisibleOverlayGlobal: string | null | undefined;
toggleInvisibleOverlayGlobal: string | null | undefined;
openJimaku?: string | null | undefined; openJimaku?: string | null | undefined;
} }
export interface RegisterGlobalShortcutsServiceOptions { export interface RegisterGlobalShortcutsServiceOptions {
shortcuts: GlobalShortcutConfig; shortcuts: GlobalShortcutConfig;
onToggleVisibleOverlay: () => void; onToggleVisibleOverlay: () => void;
onToggleInvisibleOverlay: () => void;
onOpenYomitanSettings: () => void; onOpenYomitanSettings: () => void;
onOpenJimaku?: () => void; onOpenJimaku?: () => void;
isDev: boolean; isDev: boolean;
@@ -21,9 +19,7 @@ export interface RegisterGlobalShortcutsServiceOptions {
export function registerGlobalShortcuts(options: RegisterGlobalShortcutsServiceOptions): void { export function registerGlobalShortcuts(options: RegisterGlobalShortcutsServiceOptions): void {
const visibleShortcut = options.shortcuts.toggleVisibleOverlayGlobal; const visibleShortcut = options.shortcuts.toggleVisibleOverlayGlobal;
const invisibleShortcut = options.shortcuts.toggleInvisibleOverlayGlobal;
const normalizedVisible = visibleShortcut?.replace(/\s+/g, '').toLowerCase(); const normalizedVisible = visibleShortcut?.replace(/\s+/g, '').toLowerCase();
const normalizedInvisible = invisibleShortcut?.replace(/\s+/g, '').toLowerCase();
const normalizedJimaku = options.shortcuts.openJimaku?.replace(/\s+/g, '').toLowerCase(); const normalizedJimaku = options.shortcuts.openJimaku?.replace(/\s+/g, '').toLowerCase();
const normalizedSettings = 'alt+shift+y'; 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 (options.shortcuts.openJimaku && options.onOpenJimaku) {
if ( if (
normalizedJimaku && normalizedJimaku &&
(normalizedJimaku === normalizedVisible || (normalizedJimaku === normalizedVisible || normalizedJimaku === normalizedSettings)
normalizedJimaku === normalizedInvisible ||
normalizedJimaku === normalizedSettings)
) { ) {
logger.warn( logger.warn(
'Skipped registering openJimaku because it collides with another global shortcut', 'Skipped registering openJimaku because it collides with another global shortcut',

View File

@@ -10,14 +10,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
stop: false, stop: false,
toggle: false, toggle: false,
toggleVisibleOverlay: false, toggleVisibleOverlay: false,
toggleInvisibleOverlay: false,
settings: false, settings: false,
show: false, show: false,
hide: false, hide: false,
showVisibleOverlay: false, showVisibleOverlay: false,
hideVisibleOverlay: false, hideVisibleOverlay: false,
showInvisibleOverlay: false,
hideInvisibleOverlay: false,
copySubtitle: false, copySubtitle: false,
copySubtitleMultiple: false, copySubtitleMultiple: false,
mineSentence: false, mineSentence: false,

View File

@@ -19,9 +19,6 @@ interface RuntimeAutoUpdateOptionManagerLike {
export interface RuntimeConfigLike { export interface RuntimeConfigLike {
auto_start_overlay?: boolean; auto_start_overlay?: boolean;
bind_visible_overlay_to_mpv_sub_visibility: boolean; bind_visible_overlay_to_mpv_sub_visibility: boolean;
invisibleOverlay: {
startupVisibility: 'visible' | 'hidden' | 'platform-default';
};
ankiConnect?: { ankiConnect?: {
behavior?: { behavior?: {
autoUpdateNewCards?: boolean; autoUpdateNewCards?: boolean;
@@ -155,21 +152,8 @@ function getStartupCriticalConfigErrors(config: AppReadyConfigLike): string[] {
return errors; 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 { export function shouldAutoInitializeOverlayRuntimeFromConfig(config: RuntimeConfigLike): boolean {
if (config.auto_start_overlay === true) return true; return config.auto_start_overlay === true;
if (config.invisibleOverlay.startupVisibility === 'visible') return true;
return false;
} }
export function shouldBindVisibleOverlayToMpvSubVisibility(config: RuntimeConfigLike): boolean { export function shouldBindVisibleOverlayToMpvSubVisibility(config: RuntimeConfigLike): boolean {

View File

@@ -101,20 +101,7 @@ export function loadSubtitlePosition(
const data = fs.readFileSync(positionPath, 'utf-8'); const data = fs.readFileSync(positionPath, 'utf-8');
const parsed = JSON.parse(data) as Partial<SubtitlePosition>; const parsed = JSON.parse(data) as Partial<SubtitlePosition>;
if (parsed && typeof parsed.yPercent === 'number' && Number.isFinite(parsed.yPercent)) { if (parsed && typeof parsed.yPercent === 'number' && Number.isFinite(parsed.yPercent)) {
const position: SubtitlePosition = { yPercent: parsed.yPercent }; return { 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 options.fallbackPosition; return options.fallbackPosition;
} catch (err) { } catch (err) {

View File

@@ -2,7 +2,6 @@ import { Config } from '../../types';
export interface ConfiguredShortcuts { export interface ConfiguredShortcuts {
toggleVisibleOverlayGlobal: string | null | undefined; toggleVisibleOverlayGlobal: string | null | undefined;
toggleInvisibleOverlayGlobal: string | null | undefined;
copySubtitle: string | null | undefined; copySubtitle: string | null | undefined;
copySubtitleMultiple: string | null | undefined; copySubtitleMultiple: string | null | undefined;
updateLastCardFromClipboard: string | null | undefined; updateLastCardFromClipboard: string | null | undefined;
@@ -33,10 +32,6 @@ export function resolveConfiguredShortcuts(
config.shortcuts?.toggleVisibleOverlayGlobal ?? config.shortcuts?.toggleVisibleOverlayGlobal ??
defaultConfig.shortcuts?.toggleVisibleOverlayGlobal, defaultConfig.shortcuts?.toggleVisibleOverlayGlobal,
), ),
toggleInvisibleOverlayGlobal: normalizeShortcut(
config.shortcuts?.toggleInvisibleOverlayGlobal ??
defaultConfig.shortcuts?.toggleInvisibleOverlayGlobal,
),
copySubtitle: normalizeShortcut( copySubtitle: normalizeShortcut(
config.shortcuts?.copySubtitle ?? defaultConfig.shortcuts?.copySubtitle, config.shortcuts?.copySubtitle ?? defaultConfig.shortcuts?.copySubtitle,
), ),

View File

@@ -17,9 +17,7 @@ export interface CliCommandRuntimeServiceContext {
isOverlayInitialized: () => boolean; isOverlayInitialized: () => boolean;
initializeOverlay: () => void; initializeOverlay: () => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
toggleInvisibleOverlay: () => void;
setVisibleOverlay: (visible: boolean) => void; setVisibleOverlay: (visible: boolean) => void;
setInvisibleOverlay: (visible: boolean) => void;
copyCurrentSubtitle: () => void; copyCurrentSubtitle: () => void;
startPendingMultiCopy: (timeoutMs: number) => void; startPendingMultiCopy: (timeoutMs: number) => void;
mineSentenceCard: () => Promise<void>; mineSentenceCard: () => Promise<void>;
@@ -74,9 +72,7 @@ function createCliCommandDepsFromContext(
isInitialized: context.isOverlayInitialized, isInitialized: context.isOverlayInitialized,
initialize: context.initializeOverlay, initialize: context.initializeOverlay,
toggleVisible: context.toggleVisibleOverlay, toggleVisible: context.toggleVisibleOverlay,
toggleInvisible: context.toggleInvisibleOverlay,
setVisible: context.setVisibleOverlay, setVisible: context.setVisibleOverlay,
setInvisible: context.setInvisibleOverlay,
}, },
mining: { mining: {
copyCurrentSubtitle: context.copyCurrentSubtitle, copyCurrentSubtitle: context.copyCurrentSubtitle,

View File

@@ -53,11 +53,10 @@ export function createSubsyncRuntimeDeps(params: SubsyncRuntimeDepsParams): Subs
} }
export interface MainIpcRuntimeServiceDepsParams { export interface MainIpcRuntimeServiceDepsParams {
getInvisibleWindow: IpcDepsRuntimeOptions['getInvisibleWindow'];
getMainWindow: IpcDepsRuntimeOptions['getMainWindow']; getMainWindow: IpcDepsRuntimeOptions['getMainWindow'];
getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility']; getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility'];
getInvisibleOverlayVisibility: IpcDepsRuntimeOptions['getInvisibleOverlayVisibility'];
onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed']; onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed'];
onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened'];
openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings']; openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings'];
quitApp: IpcDepsRuntimeOptions['quitApp']; quitApp: IpcDepsRuntimeOptions['quitApp'];
toggleVisibleOverlay: IpcDepsRuntimeOptions['toggleVisibleOverlay']; toggleVisibleOverlay: IpcDepsRuntimeOptions['toggleVisibleOverlay'];
@@ -65,7 +64,6 @@ export interface MainIpcRuntimeServiceDepsParams {
getCurrentSubtitleRaw: IpcDepsRuntimeOptions['getCurrentSubtitleRaw']; getCurrentSubtitleRaw: IpcDepsRuntimeOptions['getCurrentSubtitleRaw'];
getCurrentSubtitleAss: IpcDepsRuntimeOptions['getCurrentSubtitleAss']; getCurrentSubtitleAss: IpcDepsRuntimeOptions['getCurrentSubtitleAss'];
focusMainWindow?: IpcDepsRuntimeOptions['focusMainWindow']; focusMainWindow?: IpcDepsRuntimeOptions['focusMainWindow'];
getMpvSubtitleRenderMetrics: IpcDepsRuntimeOptions['getMpvSubtitleRenderMetrics'];
getSubtitlePosition: IpcDepsRuntimeOptions['getSubtitlePosition']; getSubtitlePosition: IpcDepsRuntimeOptions['getSubtitlePosition'];
getSubtitleStyle: IpcDepsRuntimeOptions['getSubtitleStyle']; getSubtitleStyle: IpcDepsRuntimeOptions['getSubtitleStyle'];
saveSubtitlePosition: IpcDepsRuntimeOptions['saveSubtitlePosition']; saveSubtitlePosition: IpcDepsRuntimeOptions['saveSubtitlePosition'];
@@ -81,7 +79,6 @@ export interface MainIpcRuntimeServiceDepsParams {
setRuntimeOption: IpcDepsRuntimeOptions['setRuntimeOption']; setRuntimeOption: IpcDepsRuntimeOptions['setRuntimeOption'];
cycleRuntimeOption: IpcDepsRuntimeOptions['cycleRuntimeOption']; cycleRuntimeOption: IpcDepsRuntimeOptions['cycleRuntimeOption'];
reportOverlayContentBounds: IpcDepsRuntimeOptions['reportOverlayContentBounds']; reportOverlayContentBounds: IpcDepsRuntimeOptions['reportOverlayContentBounds'];
reportHoveredSubtitleToken: IpcDepsRuntimeOptions['reportHoveredSubtitleToken'];
getAnilistStatus: IpcDepsRuntimeOptions['getAnilistStatus']; getAnilistStatus: IpcDepsRuntimeOptions['getAnilistStatus'];
clearAnilistToken: IpcDepsRuntimeOptions['clearAnilistToken']; clearAnilistToken: IpcDepsRuntimeOptions['clearAnilistToken'];
openAnilistSetup: IpcDepsRuntimeOptions['openAnilistSetup']; openAnilistSetup: IpcDepsRuntimeOptions['openAnilistSetup'];
@@ -132,9 +129,7 @@ export interface CliCommandRuntimeServiceDepsParams {
isInitialized: CliCommandDepsRuntimeOptions['overlay']['isInitialized']; isInitialized: CliCommandDepsRuntimeOptions['overlay']['isInitialized'];
initialize: CliCommandDepsRuntimeOptions['overlay']['initialize']; initialize: CliCommandDepsRuntimeOptions['overlay']['initialize'];
toggleVisible: CliCommandDepsRuntimeOptions['overlay']['toggleVisible']; toggleVisible: CliCommandDepsRuntimeOptions['overlay']['toggleVisible'];
toggleInvisible: CliCommandDepsRuntimeOptions['overlay']['toggleInvisible'];
setVisible: CliCommandDepsRuntimeOptions['overlay']['setVisible']; setVisible: CliCommandDepsRuntimeOptions['overlay']['setVisible'];
setInvisible: CliCommandDepsRuntimeOptions['overlay']['setInvisible'];
}; };
mining: { mining: {
copyCurrentSubtitle: CliCommandDepsRuntimeOptions['mining']['copyCurrentSubtitle']; copyCurrentSubtitle: CliCommandDepsRuntimeOptions['mining']['copyCurrentSubtitle'];
@@ -192,18 +187,16 @@ export function createMainIpcRuntimeServiceDeps(
params: MainIpcRuntimeServiceDepsParams, params: MainIpcRuntimeServiceDepsParams,
): IpcDepsRuntimeOptions { ): IpcDepsRuntimeOptions {
return { return {
getInvisibleWindow: params.getInvisibleWindow,
getMainWindow: params.getMainWindow, getMainWindow: params.getMainWindow,
getVisibleOverlayVisibility: params.getVisibleOverlayVisibility, getVisibleOverlayVisibility: params.getVisibleOverlayVisibility,
getInvisibleOverlayVisibility: params.getInvisibleOverlayVisibility,
onOverlayModalClosed: params.onOverlayModalClosed, onOverlayModalClosed: params.onOverlayModalClosed,
onOverlayModalOpened: params.onOverlayModalOpened,
openYomitanSettings: params.openYomitanSettings, openYomitanSettings: params.openYomitanSettings,
quitApp: params.quitApp, quitApp: params.quitApp,
toggleVisibleOverlay: params.toggleVisibleOverlay, toggleVisibleOverlay: params.toggleVisibleOverlay,
tokenizeCurrentSubtitle: params.tokenizeCurrentSubtitle, tokenizeCurrentSubtitle: params.tokenizeCurrentSubtitle,
getCurrentSubtitleRaw: params.getCurrentSubtitleRaw, getCurrentSubtitleRaw: params.getCurrentSubtitleRaw,
getCurrentSubtitleAss: params.getCurrentSubtitleAss, getCurrentSubtitleAss: params.getCurrentSubtitleAss,
getMpvSubtitleRenderMetrics: params.getMpvSubtitleRenderMetrics,
getSubtitlePosition: params.getSubtitlePosition, getSubtitlePosition: params.getSubtitlePosition,
getSubtitleStyle: params.getSubtitleStyle, getSubtitleStyle: params.getSubtitleStyle,
saveSubtitlePosition: params.saveSubtitlePosition, saveSubtitlePosition: params.saveSubtitlePosition,
@@ -220,7 +213,6 @@ export function createMainIpcRuntimeServiceDeps(
setRuntimeOption: params.setRuntimeOption, setRuntimeOption: params.setRuntimeOption,
cycleRuntimeOption: params.cycleRuntimeOption, cycleRuntimeOption: params.cycleRuntimeOption,
reportOverlayContentBounds: params.reportOverlayContentBounds, reportOverlayContentBounds: params.reportOverlayContentBounds,
reportHoveredSubtitleToken: params.reportHoveredSubtitleToken,
getAnilistStatus: params.getAnilistStatus, getAnilistStatus: params.getAnilistStatus,
clearAnilistToken: params.clearAnilistToken, clearAnilistToken: params.clearAnilistToken,
openAnilistSetup: params.openAnilistSetup, openAnilistSetup: params.openAnilistSetup,
@@ -279,9 +271,7 @@ export function createCliCommandRuntimeServiceDeps(
isInitialized: params.overlay.isInitialized, isInitialized: params.overlay.isInitialized,
initialize: params.overlay.initialize, initialize: params.overlay.initialize,
toggleVisible: params.overlay.toggleVisible, toggleVisible: params.overlay.toggleVisible,
toggleInvisible: params.overlay.toggleInvisible,
setVisible: params.overlay.setVisible, setVisible: params.overlay.setVisible,
setInvisible: params.overlay.setInvisible,
}, },
mining: { mining: {
copyCurrentSubtitle: params.mining.copyCurrentSubtitle, copyCurrentSubtitle: params.mining.copyCurrentSubtitle,

View File

@@ -2,50 +2,31 @@ import type { BrowserWindow } from 'electron';
import type { BaseWindowTracker } from '../window-trackers'; import type { BaseWindowTracker } from '../window-trackers';
import type { WindowGeometry } from '../types'; import type { WindowGeometry } from '../types';
import { import { updateVisibleOverlayVisibility } from '../core/services';
syncInvisibleOverlayMousePassthrough,
updateInvisibleOverlayVisibility,
updateVisibleOverlayVisibility,
} from '../core/services';
export interface OverlayVisibilityRuntimeDeps { export interface OverlayVisibilityRuntimeDeps {
getMainWindow: () => BrowserWindow | null; getMainWindow: () => BrowserWindow | null;
getInvisibleWindow: () => BrowserWindow | null;
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
getWindowTracker: () => BaseWindowTracker | null; getWindowTracker: () => BaseWindowTracker | null;
getTrackerNotReadyWarningShown: () => boolean; getTrackerNotReadyWarningShown: () => boolean;
setTrackerNotReadyWarningShown: (shown: boolean) => void; setTrackerNotReadyWarningShown: (shown: boolean) => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
ensureOverlayWindowLevel: (window: BrowserWindow) => void; ensureOverlayWindowLevel: (window: BrowserWindow) => void;
syncPrimaryOverlayWindowLayer: (layer: 'visible') => void;
enforceOverlayLayerOrder: () => void; enforceOverlayLayerOrder: () => void;
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
isMacOSPlatform: () => boolean;
showOverlayLoadingOsd: (message: string) => void;
resolveFallbackBounds: () => WindowGeometry;
} }
export interface OverlayVisibilityRuntimeService { export interface OverlayVisibilityRuntimeService {
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
} }
export function createOverlayVisibilityRuntimeService( export function createOverlayVisibilityRuntimeService(
deps: OverlayVisibilityRuntimeDeps, deps: OverlayVisibilityRuntimeDeps,
): OverlayVisibilityRuntimeService { ): OverlayVisibilityRuntimeService {
const hasInvisibleWindow = (): boolean => {
const invisibleWindow = deps.getInvisibleWindow();
return Boolean(invisibleWindow && !invisibleWindow.isDestroyed());
};
const setIgnoreMouseEvents = (
ignore: boolean,
options?: Parameters<BrowserWindow['setIgnoreMouseEvents']>[1],
): void => {
const invisibleWindow = deps.getInvisibleWindow();
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
invisibleWindow.setIgnoreMouseEvents(ignore, options);
};
return { return {
updateVisibleOverlayVisibility(): void { updateVisibleOverlayVisibility(): void {
updateVisibleOverlayVisibility({ updateVisibleOverlayVisibility({
@@ -59,31 +40,13 @@ export function createOverlayVisibilityRuntimeService(
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
deps.updateVisibleOverlayBounds(geometry), deps.updateVisibleOverlayBounds(geometry),
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
syncPrimaryOverlayWindowLayer: (layer: 'visible') =>
deps.syncPrimaryOverlayWindowLayer(layer),
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(), enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(), syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
}); isMacOSPlatform: deps.isMacOSPlatform(),
}, showOverlayLoadingOsd: (message: string) => deps.showOverlayLoadingOsd(message),
resolveFallbackBounds: () => deps.resolveFallbackBounds(),
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(),
}); });
}, },
}; };

View File

@@ -19,14 +19,12 @@ test('restore windows on activate deps builder maps all restoration callbacks',
const calls: string[] = []; const calls: string[] = [];
const deps = createBuildRestoreWindowsOnActivateMainDepsHandler({ const deps = createBuildRestoreWindowsOnActivateMainDepsHandler({
createMainWindow: () => calls.push('main'), createMainWindow: () => calls.push('main'),
createInvisibleWindow: () => calls.push('invisible'),
updateVisibleOverlayVisibility: () => calls.push('visible'), updateVisibleOverlayVisibility: () => calls.push('visible'),
updateInvisibleOverlayVisibility: () => calls.push('invisible-visible'), syncOverlayMpvSubtitleSuppression: () => calls.push('mpv-sync'),
})(); })();
deps.createMainWindow(); deps.createMainWindow();
deps.createInvisibleWindow();
deps.updateVisibleOverlayVisibility(); deps.updateVisibleOverlayVisibility();
deps.updateInvisibleOverlayVisibility(); deps.syncOverlayMpvSubtitleSuppression();
assert.deepEqual(calls, ['main', 'invisible', 'visible', 'invisible-visible']); assert.deepEqual(calls, ['main', 'visible', 'mpv-sync']);
}); });

View File

@@ -10,14 +10,12 @@ export function createBuildShouldRestoreWindowsOnActivateMainDepsHandler(deps: {
export function createBuildRestoreWindowsOnActivateMainDepsHandler(deps: { export function createBuildRestoreWindowsOnActivateMainDepsHandler(deps: {
createMainWindow: () => void; createMainWindow: () => void;
createInvisibleWindow: () => void;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void; syncOverlayMpvSubtitleSuppression: () => void;
}) { }) {
return () => ({ return () => ({
createMainWindow: () => deps.createMainWindow(), createMainWindow: () => deps.createMainWindow(),
createInvisibleWindow: () => deps.createInvisibleWindow(),
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(),
updateInvisibleOverlayVisibility: () => deps.updateInvisibleOverlayVisibility(), syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
}); });
} }

View File

@@ -50,26 +50,18 @@ test('initialize overlay runtime main deps map build options and callbacks', ()
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
initializeOverlayRuntimeCore: (value) => { initializeOverlayRuntimeCore: (value) => {
calls.push(`core:${JSON.stringify(value)}`); calls.push(`core:${JSON.stringify(value)}`);
return { invisibleOverlayVisible: true };
}, },
buildOptions: () => options, buildOptions: () => options,
setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`),
setOverlayRuntimeInitialized: (initialized) => calls.push(`set-initialized:${initialized}`), setOverlayRuntimeInitialized: (initialized) => calls.push(`set-initialized:${initialized}`),
startBackgroundWarmups: () => calls.push('warmups'), startBackgroundWarmups: () => calls.push('warmups'),
})(); })();
assert.equal(deps.isOverlayRuntimeInitialized(), false); assert.equal(deps.isOverlayRuntimeInitialized(), false);
assert.equal(deps.buildOptions(), options); assert.equal(deps.buildOptions(), options);
assert.deepEqual(deps.initializeOverlayRuntimeCore(options), { invisibleOverlayVisible: true }); assert.equal(deps.initializeOverlayRuntimeCore(options), undefined);
deps.setInvisibleOverlayVisible(true);
deps.setOverlayRuntimeInitialized(true); deps.setOverlayRuntimeInitialized(true);
deps.startBackgroundWarmups(); deps.startBackgroundWarmups();
assert.deepEqual(calls, [ assert.deepEqual(calls, ['core:{"id":"opts"}', 'set-initialized:true', 'warmups']);
'core:{"id":"opts"}',
'set-invisible:true',
'set-initialized:true',
'warmups',
]);
}); });
test('open yomitan settings main deps map async open callbacks', async () => { test('open yomitan settings main deps map async open callbacks', async () => {

View File

@@ -45,9 +45,8 @@ export function createBuildDestroyTrayMainDepsHandler<TTray>(deps: {
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOptions>(deps: { export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOptions>(deps: {
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
initializeOverlayRuntimeCore: (options: TOptions) => { invisibleOverlayVisible: boolean }; initializeOverlayRuntimeCore: (options: TOptions) => void;
buildOptions: () => TOptions; buildOptions: () => TOptions;
setInvisibleOverlayVisible: (visible: boolean) => void;
setOverlayRuntimeInitialized: (initialized: boolean) => void; setOverlayRuntimeInitialized: (initialized: boolean) => void;
startBackgroundWarmups: () => void; startBackgroundWarmups: () => void;
}) { }) {
@@ -55,7 +54,6 @@ export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOpt
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(), isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options), initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options),
buildOptions: () => deps.buildOptions(), buildOptions: () => deps.buildOptions(),
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
setOverlayRuntimeInitialized: (initialized: boolean) => setOverlayRuntimeInitialized: (initialized: boolean) =>
deps.setOverlayRuntimeInitialized(initialized), deps.setOverlayRuntimeInitialized(initialized),
startBackgroundWarmups: () => deps.startBackgroundWarmups(), startBackgroundWarmups: () => deps.startBackgroundWarmups(),

View File

@@ -18,9 +18,7 @@ test('build cli command context deps maps handlers and values', () => {
isOverlayInitialized: () => true, isOverlayInitialized: () => true,
initializeOverlay: () => calls.push('init'), initializeOverlay: () => calls.push('init'),
toggleVisibleOverlay: () => calls.push('toggle-visible'), toggleVisibleOverlay: () => calls.push('toggle-visible'),
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
setVisibleOverlay: (visible) => calls.push(`set-visible:${visible}`), setVisibleOverlay: (visible) => calls.push(`set-visible:${visible}`),
setInvisibleOverlay: (visible) => calls.push(`set-invisible:${visible}`),
copyCurrentSubtitle: () => calls.push('copy'), copyCurrentSubtitle: () => calls.push('copy'),
startPendingMultiCopy: (ms) => calls.push(`multi:${ms}`), startPendingMultiCopy: (ms) => calls.push(`multi:${ms}`),
mineSentenceCard: async () => { mineSentenceCard: async () => {

View File

@@ -15,9 +15,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
isOverlayInitialized: () => boolean; isOverlayInitialized: () => boolean;
initializeOverlay: () => void; initializeOverlay: () => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
toggleInvisibleOverlay: () => void;
setVisibleOverlay: (visible: boolean) => void; setVisibleOverlay: (visible: boolean) => void;
setInvisibleOverlay: (visible: boolean) => void;
copyCurrentSubtitle: () => void; copyCurrentSubtitle: () => void;
startPendingMultiCopy: (timeoutMs: number) => void; startPendingMultiCopy: (timeoutMs: number) => void;
mineSentenceCard: () => Promise<void>; mineSentenceCard: () => Promise<void>;
@@ -60,9 +58,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
isOverlayInitialized: deps.isOverlayInitialized, isOverlayInitialized: deps.isOverlayInitialized,
initializeOverlay: deps.initializeOverlay, initializeOverlay: deps.initializeOverlay,
toggleVisibleOverlay: deps.toggleVisibleOverlay, toggleVisibleOverlay: deps.toggleVisibleOverlay,
toggleInvisibleOverlay: deps.toggleInvisibleOverlay,
setVisibleOverlay: deps.setVisibleOverlay, setVisibleOverlay: deps.setVisibleOverlay,
setInvisibleOverlay: deps.setInvisibleOverlay,
copyCurrentSubtitle: deps.copyCurrentSubtitle, copyCurrentSubtitle: deps.copyCurrentSubtitle,
startPendingMultiCopy: deps.startPendingMultiCopy, startPendingMultiCopy: deps.startPendingMultiCopy,
mineSentenceCard: deps.mineSentenceCard, mineSentenceCard: deps.mineSentenceCard,

View File

@@ -20,9 +20,7 @@ test('cli command context factory composes main deps and context handlers', () =
showMpvOsd: (text) => calls.push(`osd:${text}`), showMpvOsd: (text) => calls.push(`osd:${text}`),
initializeOverlayRuntime: () => calls.push('init-overlay'), initializeOverlayRuntime: () => calls.push('init-overlay'),
toggleVisibleOverlay: () => calls.push('toggle-visible'), toggleVisibleOverlay: () => calls.push('toggle-visible'),
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`), setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),
setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`),
copyCurrentSubtitle: () => calls.push('copy-sub'), copyCurrentSubtitle: () => calls.push('copy-sub'),
startPendingMultiCopy: (timeoutMs) => calls.push(`multi:${timeoutMs}`), startPendingMultiCopy: (timeoutMs) => calls.push(`multi:${timeoutMs}`),
mineSentenceCard: async () => {}, mineSentenceCard: async () => {},
@@ -73,16 +71,8 @@ test('cli command context factory composes main deps and context handlers', () =
context.setSocketPath('/tmp/new.sock'); context.setSocketPath('/tmp/new.sock');
context.showOsd('hello'); context.showOsd('hello');
context.setVisibleOverlay(true); context.setVisibleOverlay(true);
context.setInvisibleOverlay(false);
context.toggleVisibleOverlay(); context.toggleVisibleOverlay();
context.toggleInvisibleOverlay();
assert.equal(appState.mpvSocketPath, '/tmp/new.sock'); assert.equal(appState.mpvSocketPath, '/tmp/new.sock');
assert.deepEqual(calls, [ assert.deepEqual(calls, ['osd:hello', 'set-visible:true', 'toggle-visible']);
'osd:hello',
'set-visible:true',
'set-invisible:false',
'toggle-visible',
'toggle-invisible',
]);
}); });

View File

@@ -23,9 +23,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
initializeOverlayRuntime: () => calls.push('init-overlay'), initializeOverlayRuntime: () => calls.push('init-overlay'),
toggleVisibleOverlay: () => calls.push('toggle-visible'), toggleVisibleOverlay: () => calls.push('toggle-visible'),
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`), setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),
setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`),
copyCurrentSubtitle: () => calls.push('copy-sub'), copyCurrentSubtitle: () => calls.push('copy-sub'),
startPendingMultiCopy: (timeoutMs) => calls.push(`multi:${timeoutMs}`), 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.showOsd('hello');
deps.initializeOverlay(); deps.initializeOverlay();
deps.setVisibleOverlay(true); deps.setVisibleOverlay(true);
deps.setInvisibleOverlay(false);
deps.printHelp(); deps.printHelp();
assert.deepEqual(calls, [ assert.deepEqual(calls, ['osd:hello', 'init-overlay', 'set-visible:true', 'help']);
'osd:hello',
'init-overlay',
'set-visible:true',
'set-invisible:false',
'help',
]);
const retry = await deps.retryAnilistQueueNow(); const retry = await deps.retryAnilistQueueNow();
assert.deepEqual(retry, { ok: true, message: 'ok' }); assert.deepEqual(retry, { ok: true, message: 'ok' });

View File

@@ -18,9 +18,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
initializeOverlayRuntime: () => void; initializeOverlayRuntime: () => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
toggleInvisibleOverlay: () => void;
setVisibleOverlayVisible: (visible: boolean) => void; setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
copyCurrentSubtitle: () => void; copyCurrentSubtitle: () => void;
startPendingMultiCopy: (timeoutMs: number) => void; startPendingMultiCopy: (timeoutMs: number) => void;
@@ -70,9 +68,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
isOverlayInitialized: () => deps.appState.overlayRuntimeInitialized, isOverlayInitialized: () => deps.appState.overlayRuntimeInitialized,
initializeOverlay: () => deps.initializeOverlayRuntime(), initializeOverlay: () => deps.initializeOverlayRuntime(),
toggleVisibleOverlay: () => deps.toggleVisibleOverlay(), toggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
toggleInvisibleOverlay: () => deps.toggleInvisibleOverlay(),
setVisibleOverlay: (visible: boolean) => deps.setVisibleOverlayVisible(visible), setVisibleOverlay: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
setInvisibleOverlay: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
copyCurrentSubtitle: () => deps.copyCurrentSubtitle(), copyCurrentSubtitle: () => deps.copyCurrentSubtitle(),
startPendingMultiCopy: (timeoutMs: number) => deps.startPendingMultiCopy(timeoutMs), startPendingMultiCopy: (timeoutMs: number) => deps.startPendingMultiCopy(timeoutMs),
mineSentenceCard: () => deps.mineSentenceCard(), mineSentenceCard: () => deps.mineSentenceCard(),

View File

@@ -24,9 +24,7 @@ function createDeps() {
isOverlayInitialized: () => true, isOverlayInitialized: () => true,
initializeOverlay: () => {}, initializeOverlay: () => {},
toggleVisibleOverlay: () => {}, toggleVisibleOverlay: () => {},
toggleInvisibleOverlay: () => {},
setVisibleOverlay: () => {}, setVisibleOverlay: () => {},
setInvisibleOverlay: () => {},
copyCurrentSubtitle: () => {}, copyCurrentSubtitle: () => {},
startPendingMultiCopy: () => {}, startPendingMultiCopy: () => {},
mineSentenceCard: async () => {}, mineSentenceCard: async () => {},

View File

@@ -20,9 +20,7 @@ export type CliCommandContextFactoryDeps = {
isOverlayInitialized: () => boolean; isOverlayInitialized: () => boolean;
initializeOverlay: () => void; initializeOverlay: () => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
toggleInvisibleOverlay: () => void;
setVisibleOverlay: (visible: boolean) => void; setVisibleOverlay: (visible: boolean) => void;
setInvisibleOverlay: (visible: boolean) => void;
copyCurrentSubtitle: () => void; copyCurrentSubtitle: () => void;
startPendingMultiCopy: (timeoutMs: number) => void; startPendingMultiCopy: (timeoutMs: number) => void;
mineSentenceCard: () => Promise<void>; mineSentenceCard: () => Promise<void>;
@@ -72,9 +70,7 @@ export function createCliCommandContext(
isOverlayInitialized: deps.isOverlayInitialized, isOverlayInitialized: deps.isOverlayInitialized,
initializeOverlay: deps.initializeOverlay, initializeOverlay: deps.initializeOverlay,
toggleVisibleOverlay: deps.toggleVisibleOverlay, toggleVisibleOverlay: deps.toggleVisibleOverlay,
toggleInvisibleOverlay: deps.toggleInvisibleOverlay,
setVisibleOverlay: deps.setVisibleOverlay, setVisibleOverlay: deps.setVisibleOverlay,
setInvisibleOverlay: deps.setInvisibleOverlay,
copyCurrentSubtitle: deps.copyCurrentSubtitle, copyCurrentSubtitle: deps.copyCurrentSubtitle,
startPendingMultiCopy: deps.startPendingMultiCopy, startPendingMultiCopy: deps.startPendingMultiCopy,
mineSentenceCard: deps.mineSentenceCard, mineSentenceCard: deps.mineSentenceCard,

View File

@@ -32,10 +32,8 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
showMpvOsd: () => {}, showMpvOsd: () => {},
}, },
mainDeps: { mainDeps: {
getInvisibleWindow: () => null,
getMainWindow: () => null, getMainWindow: () => null,
getVisibleOverlayVisibility: () => false, getVisibleOverlayVisibility: () => false,
getInvisibleOverlayVisibility: () => false,
focusMainWindow: () => {}, focusMainWindow: () => {},
onOverlayModalClosed: () => {}, onOverlayModalClosed: () => {},
openYomitanSettings: () => {}, openYomitanSettings: () => {},
@@ -44,7 +42,6 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
getCurrentSubtitleAss: () => '', getCurrentSubtitleAss: () => '',
getMpvSubtitleRenderMetrics: () => ({}) as never,
getSubtitlePosition: () => ({}) as never, getSubtitlePosition: () => ({}) as never,
getSubtitleStyle: () => ({}) as never, getSubtitleStyle: () => ({}) as never,
saveSubtitlePosition: () => {}, saveSubtitlePosition: () => {},
@@ -56,7 +53,6 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
getAnkiConnectStatus: () => false, getAnkiConnectStatus: () => false,
getRuntimeOptions: () => [], getRuntimeOptions: () => [],
reportOverlayContentBounds: () => {}, reportOverlayContentBounds: () => {},
reportHoveredSubtitleToken: () => {},
getAnilistStatus: () => ({}) as never, getAnilistStatus: () => ({}) as never,
clearAnilistToken: () => {}, clearAnilistToken: () => {},
openAnilistSetup: () => {}, openAnilistSetup: () => {},

View File

@@ -68,12 +68,14 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
scheduleQuitCheck: () => {}, scheduleQuitCheck: () => {},
quitApp: () => {}, quitApp: () => {},
reportJellyfinRemoteStopped: () => {}, reportJellyfinRemoteStopped: () => {},
syncOverlayMpvSubtitleSuppression: () => {},
maybeRunAnilistPostWatchUpdate: async () => {}, maybeRunAnilistPostWatchUpdate: async () => {},
logSubtitleTimingError: () => {}, logSubtitleTimingError: () => {},
broadcastToOverlayWindows: () => {}, broadcastToOverlayWindows: () => {},
onSubtitleChange: () => {}, onSubtitleChange: () => {},
refreshDiscordPresence: () => {}, refreshDiscordPresence: () => {},
updateCurrentMediaPath: () => {}, updateCurrentMediaPath: () => {},
restoreMpvSubVisibilityForInvisibleOverlay: () => {},
getCurrentAnilistMediaKey: () => null, getCurrentAnilistMediaKey: () => null,
resetAnilistMediaTracking: () => {}, resetAnilistMediaTracking: () => {},
maybeProbeAnilistDuration: () => {}, maybeProbeAnilistDuration: () => {},

View File

@@ -14,7 +14,6 @@ test('composeShortcutRuntimes returns callable shortcut runtime handlers', () =>
getConfiguredShortcuts: () => ({}) as never, getConfiguredShortcuts: () => ({}) as never,
registerGlobalShortcutsCore: () => {}, registerGlobalShortcutsCore: () => {},
toggleVisibleOverlay: () => {}, toggleVisibleOverlay: () => {},
toggleInvisibleOverlay: () => {},
openYomitanSettings: () => {}, openYomitanSettings: () => {},
isDev: false, isDev: false,
getMainWindow: () => null, getMainWindow: () => null,

View File

@@ -1,7 +1,6 @@
import type { RuntimeOptionsManager } from '../../runtime-options'; import type { RuntimeOptionsManager } from '../../runtime-options';
import type { JimakuApiResponse, JimakuLanguagePreference, ResolvedConfig } from '../../types'; import type { JimakuApiResponse, JimakuLanguagePreference, ResolvedConfig } from '../../types';
import { import {
getInitialInvisibleOverlayVisibility as getInitialInvisibleOverlayVisibilityCore,
getJimakuLanguagePreference as getJimakuLanguagePreferenceCore, getJimakuLanguagePreference as getJimakuLanguagePreferenceCore,
getJimakuMaxEntryResults as getJimakuMaxEntryResultsCore, getJimakuMaxEntryResults as getJimakuMaxEntryResultsCore,
isAutoUpdateEnabledRuntime as isAutoUpdateEnabledRuntimeCore, isAutoUpdateEnabledRuntime as isAutoUpdateEnabledRuntimeCore,
@@ -14,14 +13,12 @@ import {
export type ConfigDerivedRuntimeDeps = { export type ConfigDerivedRuntimeDeps = {
getResolvedConfig: () => ResolvedConfig; getResolvedConfig: () => ResolvedConfig;
getRuntimeOptionsManager: () => RuntimeOptionsManager | null; getRuntimeOptionsManager: () => RuntimeOptionsManager | null;
platform: NodeJS.Platform;
defaultJimakuLanguagePreference: JimakuLanguagePreference; defaultJimakuLanguagePreference: JimakuLanguagePreference;
defaultJimakuMaxEntryResults: number; defaultJimakuMaxEntryResults: number;
defaultJimakuApiBaseUrl: string; defaultJimakuApiBaseUrl: string;
}; };
export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): { export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
getInitialInvisibleOverlayVisibility: () => boolean;
shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean; shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean; shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isAutoUpdateEnabledRuntime: () => boolean; isAutoUpdateEnabledRuntime: () => boolean;
@@ -34,8 +31,6 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
) => Promise<JimakuApiResponse<T>>; ) => Promise<JimakuApiResponse<T>>;
} { } {
return { return {
getInitialInvisibleOverlayVisibility: () =>
getInitialInvisibleOverlayVisibilityCore(deps.getResolvedConfig(), deps.platform),
shouldAutoInitializeOverlayRuntimeFromConfig: () => shouldAutoInitializeOverlayRuntimeFromConfig: () =>
shouldAutoInitializeOverlayRuntimeFromConfigCore(deps.getResolvedConfig()), shouldAutoInitializeOverlayRuntimeFromConfigCore(deps.getResolvedConfig()),
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility: () =>

View File

@@ -15,9 +15,7 @@ test('field grouping overlay main deps builder maps window visibility and resolv
}, },
}), }),
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`), setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
setInvisibleOverlayVisible: (visible) => calls.push(`invisible:${visible}`),
getResolver: () => resolver, getResolver: () => resolver,
setResolver: (nextResolver) => { setResolver: (nextResolver) => {
calls.push(`set-resolver:${nextResolver ? 'set' : 'null'}`); 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.getMainWindow()?.isDestroyed(), false);
assert.equal(deps.getVisibleOverlayVisible(), true); assert.equal(deps.getVisibleOverlayVisible(), true);
assert.equal(deps.getInvisibleOverlayVisible(), false);
assert.equal(deps.getResolver(), resolver); assert.equal(deps.getResolver(), resolver);
assert.equal(deps.getRestoreVisibleOverlayOnModalClose(), modalSet); assert.equal(deps.getRestoreVisibleOverlayOnModalClose(), modalSet);
deps.setVisibleOverlayVisible(true); deps.setVisibleOverlayVisible(true);
deps.setInvisibleOverlayVisible(false);
deps.setResolver(null); deps.setResolver(null);
assert.equal(deps.sendToVisibleOverlay('kiku:open', 1), true); assert.equal(deps.sendToVisibleOverlay('kiku:open', 1), true);
assert.deepEqual(calls, [ assert.deepEqual(calls, ['visible:true', 'set-resolver:null', 'send:kiku:open:1']);
'visible:true',
'invisible:false',
'set-resolver:null',
'send:kiku:open:1',
]);
}); });

View File

@@ -24,9 +24,7 @@ export function createBuildFieldGroupingOverlayMainDepsHandler<TModal extends st
return (): BuiltFieldGroupingOverlayMainDeps<TModal> => ({ return (): BuiltFieldGroupingOverlayMainDeps<TModal> => ({
getMainWindow: () => deps.getMainWindow(), getMainWindow: () => deps.getMainWindow(),
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible), setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
getResolver: () => deps.getResolver(), getResolver: () => deps.getResolver(),
setResolver: (resolver) => deps.setResolver(resolver), setResolver: (resolver) => deps.setResolver(resolver),
getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(), getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(),

View File

@@ -28,7 +28,6 @@ test('register global shortcuts main deps map callbacks and flags', () => {
getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never), getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never),
registerGlobalShortcutsCore: () => calls.push('register'), registerGlobalShortcutsCore: () => calls.push('register'),
toggleVisibleOverlay: () => calls.push('toggle-visible'), toggleVisibleOverlay: () => calls.push('toggle-visible'),
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
openYomitanSettings: () => calls.push('open-yomitan'), openYomitanSettings: () => calls.push('open-yomitan'),
isDev: true, isDev: true,
getMainWindow: () => mainWindow as never, getMainWindow: () => mainWindow as never,
@@ -38,17 +37,15 @@ test('register global shortcuts main deps map callbacks and flags', () => {
deps.registerGlobalShortcutsCore({ deps.registerGlobalShortcutsCore({
shortcuts: deps.getConfiguredShortcuts(), shortcuts: deps.getConfiguredShortcuts(),
onToggleVisibleOverlay: () => undefined, onToggleVisibleOverlay: () => undefined,
onToggleInvisibleOverlay: () => undefined,
onOpenYomitanSettings: () => undefined, onOpenYomitanSettings: () => undefined,
isDev: deps.isDev, isDev: deps.isDev,
getMainWindow: deps.getMainWindow, getMainWindow: deps.getMainWindow,
}); });
deps.onToggleVisibleOverlay(); deps.onToggleVisibleOverlay();
deps.onToggleInvisibleOverlay();
deps.onOpenYomitanSettings(); deps.onOpenYomitanSettings();
assert.equal(deps.isDev, true); assert.equal(deps.isDev, true);
assert.deepEqual(deps.getMainWindow(), mainWindow); 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', () => { test('refresh global shortcuts main deps map passthrough handlers', () => {

View File

@@ -19,7 +19,6 @@ export function createBuildRegisterGlobalShortcutsMainDepsHandler(deps: {
getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts']; getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts'];
registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void; registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
toggleInvisibleOverlay: () => void;
openYomitanSettings: () => void; openYomitanSettings: () => void;
isDev: boolean; isDev: boolean;
getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow']; getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow'];
@@ -29,7 +28,6 @@ export function createBuildRegisterGlobalShortcutsMainDepsHandler(deps: {
registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) =>
deps.registerGlobalShortcutsCore(options), deps.registerGlobalShortcutsCore(options),
onToggleVisibleOverlay: () => deps.toggleVisibleOverlay(), onToggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
onToggleInvisibleOverlay: () => deps.toggleInvisibleOverlay(),
onOpenYomitanSettings: () => deps.openYomitanSettings(), onOpenYomitanSettings: () => deps.openYomitanSettings(),
isDev: deps.isDev, isDev: deps.isDev,
getMainWindow: deps.getMainWindow, getMainWindow: deps.getMainWindow,

View File

@@ -6,7 +6,6 @@ import { createGlobalShortcutsRuntimeHandlers } from './global-shortcuts-runtime
function createShortcuts(): ConfiguredShortcuts { function createShortcuts(): ConfiguredShortcuts {
return { return {
toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O', toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O',
toggleInvisibleOverlayGlobal: 'CommandOrControl+Shift+I',
copySubtitle: 's', copySubtitle: 's',
copySubtitleMultiple: 'CommandOrControl+s', copySubtitleMultiple: 'CommandOrControl+s',
updateLastCardFromClipboard: 'c', updateLastCardFromClipboard: 'c',
@@ -38,7 +37,6 @@ test('global shortcuts runtime handlers compose get/register/refresh flow', () =
assert.equal(options.shortcuts, shortcuts); assert.equal(options.shortcuts, shortcuts);
}, },
toggleVisibleOverlay: () => calls.push('toggle-visible'), toggleVisibleOverlay: () => calls.push('toggle-visible'),
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
openYomitanSettings: () => calls.push('open-yomitan'), openYomitanSettings: () => calls.push('open-yomitan'),
isDev: false, isDev: false,
getMainWindow: () => null, getMainWindow: () => null,

View File

@@ -10,7 +10,6 @@ import type { ConfiguredShortcuts } from '../../core/utils/shortcut-config';
function createShortcuts(): ConfiguredShortcuts { function createShortcuts(): ConfiguredShortcuts {
return { return {
toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O', toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O',
toggleInvisibleOverlayGlobal: 'CommandOrControl+Shift+I',
copySubtitle: 's', copySubtitle: 's',
copySubtitleMultiple: 'CommandOrControl+s', copySubtitleMultiple: 'CommandOrControl+s',
updateLastCardFromClipboard: 'c', updateLastCardFromClipboard: 'c',
@@ -58,18 +57,16 @@ test('register global shortcuts handler passes through callbacks and shortcuts',
assert.equal(options.isDev, true); assert.equal(options.isDev, true);
assert.equal(options.getMainWindow(), mainWindow); assert.equal(options.getMainWindow(), mainWindow);
options.onToggleVisibleOverlay(); options.onToggleVisibleOverlay();
options.onToggleInvisibleOverlay();
options.onOpenYomitanSettings(); options.onOpenYomitanSettings();
}, },
onToggleVisibleOverlay: () => calls.push('toggle-visible'), onToggleVisibleOverlay: () => calls.push('toggle-visible'),
onToggleInvisibleOverlay: () => calls.push('toggle-invisible'),
onOpenYomitanSettings: () => calls.push('open-yomitan'), onOpenYomitanSettings: () => calls.push('open-yomitan'),
isDev: true, isDev: true,
getMainWindow: () => mainWindow, getMainWindow: () => mainWindow,
}); });
registerGlobalShortcuts(); 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', () => { test('refresh global and overlay shortcuts unregisters then re-registers', () => {

View File

@@ -18,7 +18,6 @@ export function createRegisterGlobalShortcutsHandler(deps: {
getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts']; getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts'];
registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void; registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void;
onToggleVisibleOverlay: () => void; onToggleVisibleOverlay: () => void;
onToggleInvisibleOverlay: () => void;
onOpenYomitanSettings: () => void; onOpenYomitanSettings: () => void;
isDev: boolean; isDev: boolean;
getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow']; getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow'];
@@ -27,7 +26,6 @@ export function createRegisterGlobalShortcutsHandler(deps: {
deps.registerGlobalShortcutsCore({ deps.registerGlobalShortcutsCore({
shortcuts: deps.getConfiguredShortcuts(), shortcuts: deps.getConfiguredShortcuts(),
onToggleVisibleOverlay: deps.onToggleVisibleOverlay, onToggleVisibleOverlay: deps.onToggleVisibleOverlay,
onToggleInvisibleOverlay: deps.onToggleInvisibleOverlay,
onOpenYomitanSettings: deps.onOpenYomitanSettings, onOpenYomitanSettings: deps.onOpenYomitanSettings,
isDev: deps.isDev, isDev: deps.isDev,
getMainWindow: deps.getMainWindow, getMainWindow: deps.getMainWindow,

View File

@@ -24,9 +24,6 @@ function createArgs(overrides: Partial<CliArgs> = {}): CliArgs {
toggleOverlay: false, toggleOverlay: false,
hideOverlay: false, hideOverlay: false,
showOverlay: false, showOverlay: false,
toggleInvisibleOverlay: false,
hideInvisibleOverlay: false,
showInvisibleOverlay: false,
copyCurrentSubtitle: false, copyCurrentSubtitle: false,
multiCopy: false, multiCopy: false,
mineSentence: false, mineSentence: false,

View File

@@ -11,6 +11,7 @@ test('mpv connection handler reports stop and quits when disconnect guard passes
const handler = createHandleMpvConnectionChangeHandler({ const handler = createHandleMpvConnectionChangeHandler({
reportJellyfinRemoteStopped: () => calls.push('report-stop'), reportJellyfinRemoteStopped: () => calls.push('report-stop'),
refreshDiscordPresence: () => calls.push('presence-refresh'), refreshDiscordPresence: () => calls.push('presence-refresh'),
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
hasInitialJellyfinPlayArg: () => true, hasInitialJellyfinPlayArg: () => true,
isOverlayRuntimeInitialized: () => false, isOverlayRuntimeInitialized: () => false,
isQuitOnDisconnectArmed: () => true, 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']); 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', () => { test('mpv subtitle timing handler ignores blank subtitle lines', () => {
const calls: string[] = []; const calls: string[] = [];
const handler = createHandleMpvSubtitleTimingHandler({ const handler = createHandleMpvSubtitleTimingHandler({

View File

@@ -18,6 +18,7 @@ type MpvEventClient = {
export function createHandleMpvConnectionChangeHandler(deps: { export function createHandleMpvConnectionChangeHandler(deps: {
reportJellyfinRemoteStopped: () => void; reportJellyfinRemoteStopped: () => void;
refreshDiscordPresence: () => void; refreshDiscordPresence: () => void;
syncOverlayMpvSubtitleSuppression: () => void;
hasInitialJellyfinPlayArg: () => boolean; hasInitialJellyfinPlayArg: () => boolean;
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
isQuitOnDisconnectArmed: () => boolean; isQuitOnDisconnectArmed: () => boolean;
@@ -27,7 +28,10 @@ export function createHandleMpvConnectionChangeHandler(deps: {
}) { }) {
return ({ connected }: { connected: boolean }): void => { return ({ connected }: { connected: boolean }): void => {
deps.refreshDiscordPresence(); deps.refreshDiscordPresence();
if (connected) return; if (connected) {
deps.syncOverlayMpvSubtitleSuppression();
return;
}
deps.reportJellyfinRemoteStopped(); deps.reportJellyfinRemoteStopped();
if (!deps.hasInitialJellyfinPlayArg()) return; if (!deps.hasInitialJellyfinPlayArg()) return;
if (deps.isOverlayRuntimeInitialized()) return; if (deps.isOverlayRuntimeInitialized()) return;

View File

@@ -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);
});

View File

@@ -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) });
};
}

View File

@@ -51,6 +51,7 @@ test('media path change handler reports stop for empty path and probes media key
const handler = createHandleMpvMediaPathChangeHandler({ const handler = createHandleMpvMediaPathChangeHandler({
updateCurrentMediaPath: (path) => calls.push(`path:${path}`), updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
reportJellyfinRemoteStopped: () => calls.push('stopped'), reportJellyfinRemoteStopped: () => calls.push('stopped'),
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'),
getCurrentAnilistMediaKey: () => 'show:1', getCurrentAnilistMediaKey: () => 'show:1',
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`), resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`),
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${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, [ assert.deepEqual(calls, [
'path:', 'path:',
'stopped', 'stopped',
'restore-mpv-sub',
'reset:show:1', 'reset:show:1',
'probe:show:1', 'probe:show:1',
'guess:show:1', 'guess:show:1',

View File

@@ -19,12 +19,10 @@ test('overlay content measurement store main deps builder maps callbacks', () =>
test('overlay modal runtime main deps builder maps window resolvers', () => { test('overlay modal runtime main deps builder maps window resolvers', () => {
const mainWindow = { id: 'main' }; const mainWindow = { id: 'main' };
const invisibleWindow = { id: 'invisible' };
const modalWindow = { id: 'modal' }; const modalWindow = { id: 'modal' };
const calls: string[] = []; const calls: string[] = [];
const deps = createBuildOverlayModalRuntimeMainDepsHandler({ const deps = createBuildOverlayModalRuntimeMainDepsHandler({
getMainWindow: () => mainWindow as never, getMainWindow: () => mainWindow as never,
getInvisibleWindow: () => invisibleWindow as never,
getModalWindow: () => modalWindow as never, getModalWindow: () => modalWindow as never,
createModalWindow: () => modalWindow as never, createModalWindow: () => modalWindow as never,
getModalGeometry: () => ({ x: 1, y: 2, width: 3, height: 4 }), 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.getMainWindow(), mainWindow);
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
assert.equal(deps.getModalWindow(), modalWindow); assert.equal(deps.getModalWindow(), modalWindow);
assert.equal(deps.createModalWindow(), modalWindow); assert.equal(deps.createModalWindow(), modalWindow);
assert.deepEqual(deps.getModalGeometry(), { x: 1, y: 2, width: 3, height: 4 }); assert.deepEqual(deps.getModalGeometry(), { x: 1, y: 2, width: 3, height: 4 });

View File

@@ -19,7 +19,6 @@ export function createBuildOverlayModalRuntimeMainDepsHandler(
) { ) {
return (): OverlayWindowResolver => ({ return (): OverlayWindowResolver => ({
getMainWindow: () => deps.getMainWindow(), getMainWindow: () => deps.getMainWindow(),
getInvisibleWindow: () => deps.getInvisibleWindow(),
getModalWindow: () => deps.getModalWindow(), getModalWindow: () => deps.getModalWindow(),
createModalWindow: () => deps.createModalWindow(), createModalWindow: () => deps.createModalWindow(),
getModalGeometry: () => deps.getModalGeometry(), getModalGeometry: () => deps.getModalGeometry(),

View File

@@ -15,7 +15,6 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h
ankiIntegration: null as unknown, ankiIntegration: null as unknown,
}; };
let initialized = false; let initialized = false;
let invisibleOverlayVisible = false;
let warmupsStarted = 0; let warmupsStarted = 0;
const { initializeOverlayRuntime } = createOverlayRuntimeBootstrapHandlers({ const { initializeOverlayRuntime } = createOverlayRuntimeBootstrapHandlers({
@@ -23,21 +22,16 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h
appState, appState,
overlayManager: { overlayManager: {
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false,
}, },
overlayVisibilityRuntime: { overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => {}, updateVisibleOverlayVisibility: () => {},
updateInvisibleOverlayVisibility: () => {},
}, },
overlayShortcutsRuntime: { overlayShortcutsRuntime: {
syncOverlayShortcuts: () => {}, syncOverlayShortcuts: () => {},
}, },
getInitialInvisibleOverlayVisibility: () => false,
createMainWindow: () => {}, createMainWindow: () => {},
createInvisibleWindow: () => {},
registerGlobalShortcuts: () => {}, registerGlobalShortcuts: () => {},
updateVisibleOverlayBounds: () => {}, updateVisibleOverlayBounds: () => {},
updateInvisibleOverlayBounds: () => {},
getOverlayWindows: () => [], getOverlayWindows: () => [],
getResolvedConfig: () => ({}), getResolvedConfig: () => ({}),
showDesktopNotification: () => {}, showDesktopNotification: () => {},
@@ -52,10 +46,7 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h
}, },
initializeOverlayRuntimeBootstrapDeps: { initializeOverlayRuntimeBootstrapDeps: {
isOverlayRuntimeInitialized: () => initialized, isOverlayRuntimeInitialized: () => initialized,
initializeOverlayRuntimeCore: () => ({ invisibleOverlayVisible: true }), initializeOverlayRuntimeCore: () => {},
setInvisibleOverlayVisible: (visible) => {
invisibleOverlayVisible = visible;
},
setOverlayRuntimeInitialized: (next) => { setOverlayRuntimeInitialized: (next) => {
initialized = next; initialized = next;
}, },
@@ -68,7 +59,6 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h
initializeOverlayRuntime(); initializeOverlayRuntime();
initializeOverlayRuntime(); initializeOverlayRuntime();
assert.equal(invisibleOverlayVisible, true);
assert.equal(initialized, true); assert.equal(initialized, true);
assert.equal(warmupsStarted, 1); assert.equal(warmupsStarted, 1);
}); });

View File

@@ -8,10 +8,8 @@ test('overlay runtime bootstrap no-ops when already initialized', () => {
isOverlayRuntimeInitialized: () => true, isOverlayRuntimeInitialized: () => true,
initializeOverlayRuntimeCore: () => { initializeOverlayRuntimeCore: () => {
coreCalls += 1; coreCalls += 1;
return { invisibleOverlayVisible: false };
}, },
buildOptions: () => ({} as never), buildOptions: () => ({} as never),
setInvisibleOverlayVisible: () => {},
setOverlayRuntimeInitialized: () => {}, setOverlayRuntimeInitialized: () => {},
startBackgroundWarmups: () => {}, startBackgroundWarmups: () => {},
}); });
@@ -27,15 +25,11 @@ test('overlay runtime bootstrap runs core init and applies post-init state', ()
isOverlayRuntimeInitialized: () => initialized, isOverlayRuntimeInitialized: () => initialized,
initializeOverlayRuntimeCore: () => { initializeOverlayRuntimeCore: () => {
calls.push('core'); calls.push('core');
return { invisibleOverlayVisible: true };
}, },
buildOptions: () => { buildOptions: () => {
calls.push('options'); calls.push('options');
return {} as never; return {} as never;
}, },
setInvisibleOverlayVisible: (visible) => {
calls.push(`invisible:${visible ? 'yes' : 'no'}`);
},
setOverlayRuntimeInitialized: (value) => { setOverlayRuntimeInitialized: (value) => {
initialized = value; initialized = value;
calls.push(`initialized:${value ? 'yes' : 'no'}`); calls.push(`initialized:${value ? 'yes' : 'no'}`);
@@ -47,5 +41,5 @@ test('overlay runtime bootstrap runs core init and applies post-init state', ()
initialize(); initialize();
assert.equal(initialized, true); assert.equal(initialized, true);
assert.deepEqual(calls, ['options', 'core', 'invisible:yes', 'initialized:yes', 'warmups']); assert.deepEqual(calls, ['options', 'core', 'initialized:yes', 'warmups']);
}); });

View File

@@ -9,16 +9,11 @@ import type {
type InitializeOverlayRuntimeCore = (options: { type InitializeOverlayRuntimeCore = (options: {
backendOverride: string | null; backendOverride: string | null;
getInitialInvisibleOverlayVisibility: () => boolean;
createMainWindow: () => void; createMainWindow: () => void;
createInvisibleWindow: () => void;
registerGlobalShortcuts: () => void; registerGlobalShortcuts: () => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
isInvisibleOverlayVisible: () => boolean;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
getOverlayWindows: () => BrowserWindow[]; getOverlayWindows: () => BrowserWindow[];
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
setWindowTracker: (tracker: BaseWindowTracker | null) => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void;
@@ -35,20 +30,18 @@ type InitializeOverlayRuntimeCore = (options: {
data: KikuFieldGroupingRequestData, data: KikuFieldGroupingRequestData,
) => Promise<KikuFieldGroupingChoice>; ) => Promise<KikuFieldGroupingChoice>;
getKnownWordCacheStatePath: () => string; getKnownWordCacheStatePath: () => string;
}) => { invisibleOverlayVisible: boolean }; }) => void;
export function createInitializeOverlayRuntimeHandler(deps: { export function createInitializeOverlayRuntimeHandler(deps: {
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
initializeOverlayRuntimeCore: InitializeOverlayRuntimeCore; initializeOverlayRuntimeCore: InitializeOverlayRuntimeCore;
buildOptions: () => Parameters<InitializeOverlayRuntimeCore>[0]; buildOptions: () => Parameters<InitializeOverlayRuntimeCore>[0];
setInvisibleOverlayVisible: (visible: boolean) => void;
setOverlayRuntimeInitialized: (initialized: boolean) => void; setOverlayRuntimeInitialized: (initialized: boolean) => void;
startBackgroundWarmups: () => void; startBackgroundWarmups: () => void;
}) { }) {
return (): void => { return (): void => {
if (deps.isOverlayRuntimeInitialized()) return; if (deps.isOverlayRuntimeInitialized()) return;
const result = deps.initializeOverlayRuntimeCore(deps.buildOptions()); deps.initializeOverlayRuntimeCore(deps.buildOptions());
deps.setInvisibleOverlayVisible(result.invisibleOverlayVisible);
deps.setOverlayRuntimeInitialized(true); deps.setOverlayRuntimeInitialized(true);
deps.startBackgroundWarmups(); deps.startBackgroundWarmups();
}; };

View File

@@ -57,14 +57,12 @@ test('set overlay debug visualization main deps builder maps callbacks', () => {
setOverlayDebugVisualizationEnabledRuntime: () => calls.push('set-runtime'), setOverlayDebugVisualizationEnabledRuntime: () => calls.push('set-runtime'),
getCurrentEnabled: () => false, getCurrentEnabled: () => false,
setCurrentEnabled: () => calls.push('set-current'), setCurrentEnabled: () => calls.push('set-current'),
broadcastToOverlayWindows: () => calls.push('broadcast'),
})(); })();
deps.setOverlayDebugVisualizationEnabledRuntime(false, true, () => {}, () => {}); deps.setOverlayDebugVisualizationEnabledRuntime(false, true, () => {});
assert.equal(deps.getCurrentEnabled(), false); assert.equal(deps.getCurrentEnabled(), false);
deps.setCurrentEnabled(true); deps.setCurrentEnabled(true);
deps.broadcastToOverlayWindows('overlay:debug'); assert.deepEqual(calls, ['set-runtime', 'set-current']);
assert.deepEqual(calls, ['set-runtime', 'set-current', 'broadcast']);
}); });
test('open runtime options palette main deps builder maps callbacks', () => { test('open runtime options palette main deps builder maps callbacks', () => {

View File

@@ -65,18 +65,14 @@ export function createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler(
currentEnabled, currentEnabled,
nextEnabled, nextEnabled,
setCurrentEnabled, setCurrentEnabled,
broadcastToOverlayWindows,
) => ) =>
deps.setOverlayDebugVisualizationEnabledRuntime( deps.setOverlayDebugVisualizationEnabledRuntime(
currentEnabled, currentEnabled,
nextEnabled, nextEnabled,
setCurrentEnabled, setCurrentEnabled,
broadcastToOverlayWindows,
), ),
getCurrentEnabled: () => deps.getCurrentEnabled(), getCurrentEnabled: () => deps.getCurrentEnabled(),
setCurrentEnabled: (enabled: boolean) => deps.setCurrentEnabled(enabled), setCurrentEnabled: (enabled: boolean) => deps.setCurrentEnabled(enabled),
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) =>
deps.broadcastToOverlayWindows(channel, ...args),
}); });
} }

View File

@@ -104,22 +104,21 @@ test('set overlay debug visualization enabled delegates with current state and b
const calls: string[] = []; const calls: string[] = [];
let current = false; let current = false;
const setEnabled = createSetOverlayDebugVisualizationEnabledHandler({ const setEnabled = createSetOverlayDebugVisualizationEnabledHandler({
setOverlayDebugVisualizationEnabledRuntime: (curr, next, setCurrent, broadcast) => { setOverlayDebugVisualizationEnabledRuntime: (curr, next, setCurrent) => {
calls.push(`runtime:${curr}->${next}`); calls.push(`runtime:${curr}->${next}`);
setCurrent(next); setCurrent(next);
broadcast('overlay-debug:set', next); // no renderer-level side effects for this legacy debug path.
}, },
getCurrentEnabled: () => current, getCurrentEnabled: () => current,
setCurrentEnabled: (enabled) => { setCurrentEnabled: (enabled) => {
current = enabled; current = enabled;
calls.push(`set:${enabled}`); calls.push(`set:${enabled}`);
}, },
broadcastToOverlayWindows: (channel, value) => calls.push(`emit:${channel}:${value}`),
}); });
setEnabled(true); setEnabled(true);
assert.equal(current, 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', () => { test('open runtime options palette handler delegates to runtime', () => {

View File

@@ -65,18 +65,15 @@ export function createSetOverlayDebugVisualizationEnabledHandler(deps: {
currentEnabled: boolean, currentEnabled: boolean,
nextEnabled: boolean, nextEnabled: boolean,
setCurrentEnabled: (enabled: boolean) => void, setCurrentEnabled: (enabled: boolean) => void,
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
) => void; ) => void;
getCurrentEnabled: () => boolean; getCurrentEnabled: () => boolean;
setCurrentEnabled: (enabled: boolean) => void; setCurrentEnabled: (enabled: boolean) => void;
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
}) { }) {
return (enabled: boolean): void => { return (enabled: boolean): void => {
deps.setOverlayDebugVisualizationEnabledRuntime( deps.setOverlayDebugVisualizationEnabledRuntime(
deps.getCurrentEnabled(), deps.getCurrentEnabled(),
enabled, enabled,
(next) => deps.setCurrentEnabled(next), (next) => deps.setCurrentEnabled(next),
(channel, ...args) => deps.broadcastToOverlayWindows(channel, ...args),
); );
}; };
} }

View File

@@ -19,21 +19,16 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
appState, appState,
overlayManager: { overlayManager: {
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false,
}, },
overlayVisibilityRuntime: { overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => calls.push('update-visible'), updateVisibleOverlayVisibility: () => calls.push('update-visible'),
updateInvisibleOverlayVisibility: () => calls.push('update-invisible'),
}, },
overlayShortcutsRuntime: { overlayShortcutsRuntime: {
syncOverlayShortcuts: () => calls.push('sync-shortcuts'), syncOverlayShortcuts: () => calls.push('sync-shortcuts'),
}, },
getInitialInvisibleOverlayVisibility: () => true,
createMainWindow: () => calls.push('create-main'), createMainWindow: () => calls.push('create-main'),
createInvisibleWindow: () => calls.push('create-invisible'),
registerGlobalShortcuts: () => calls.push('register-shortcuts'), registerGlobalShortcuts: () => calls.push('register-shortcuts'),
updateVisibleOverlayBounds: () => calls.push('visible-bounds'), updateVisibleOverlayBounds: () => calls.push('visible-bounds'),
updateInvisibleOverlayBounds: () => calls.push('invisible-bounds'),
getOverlayWindows: () => [], getOverlayWindows: () => [],
getResolvedConfig: () => ({}), getResolvedConfig: () => ({}),
showDesktopNotification: () => calls.push('notify'), showDesktopNotification: () => calls.push('notify'),
@@ -48,19 +43,14 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
const deps = build(); const deps = build();
assert.equal(deps.getBackendOverride(), 'x11'); assert.equal(deps.getBackendOverride(), 'x11');
assert.equal(deps.getInitialInvisibleOverlayVisibility(), true);
assert.equal(deps.isVisibleOverlayVisible(), true); assert.equal(deps.isVisibleOverlayVisible(), true);
assert.equal(deps.isInvisibleOverlayVisible(), false);
assert.equal(deps.getMpvSocketPath(), '/tmp/mpv.sock'); assert.equal(deps.getMpvSocketPath(), '/tmp/mpv.sock');
assert.equal(deps.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json'); assert.equal(deps.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json');
deps.createMainWindow(); deps.createMainWindow();
deps.createInvisibleWindow();
deps.registerGlobalShortcuts(); deps.registerGlobalShortcuts();
deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
deps.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
deps.updateVisibleOverlayVisibility(); deps.updateVisibleOverlayVisibility();
deps.updateInvisibleOverlayVisibility();
deps.syncOverlayShortcuts(); deps.syncOverlayShortcuts();
deps.showDesktopNotification('title', {}); deps.showDesktopNotification('title', {});
@@ -73,12 +63,9 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'create-main', 'create-main',
'create-invisible',
'register-shortcuts', 'register-shortcuts',
'visible-bounds', 'visible-bounds',
'invisible-bounds',
'update-visible', 'update-visible',
'update-invisible',
'sync-shortcuts', 'sync-shortcuts',
'notify', 'notify',
]); ]);

View File

@@ -17,18 +17,14 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
}; };
overlayManager: { overlayManager: {
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
}; };
overlayVisibilityRuntime: { overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
}; };
overlayShortcutsRuntime: { overlayShortcutsRuntime: {
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
}; };
getInitialInvisibleOverlayVisibility: () => boolean;
createMainWindow: () => void; createMainWindow: () => void;
createInvisibleWindow: () => void;
registerGlobalShortcuts: () => void; registerGlobalShortcuts: () => void;
updateVisibleOverlayBounds: (geometry: { updateVisibleOverlayBounds: (geometry: {
x: number; x: number;
@@ -36,12 +32,6 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
width: number; width: number;
height: number; height: number;
}) => void; }) => void;
updateInvisibleOverlayBounds: (geometry: {
x: number;
y: number;
width: number;
height: number;
}) => void;
getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows']; getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows'];
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void; showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
@@ -50,9 +40,7 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
}) { }) {
return (): OverlayRuntimeOptionsMainDeps => ({ return (): OverlayRuntimeOptionsMainDeps => ({
getBackendOverride: () => deps.appState.backendOverride, getBackendOverride: () => deps.appState.backendOverride,
getInitialInvisibleOverlayVisibility: () => deps.getInitialInvisibleOverlayVisibility(),
createMainWindow: () => deps.createMainWindow(), createMainWindow: () => deps.createMainWindow(),
createInvisibleWindow: () => deps.createInvisibleWindow(),
registerGlobalShortcuts: () => deps.registerGlobalShortcuts(), registerGlobalShortcuts: () => deps.registerGlobalShortcuts(),
updateVisibleOverlayBounds: (geometry: { updateVisibleOverlayBounds: (geometry: {
x: number; x: number;
@@ -60,18 +48,9 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
width: number; width: number;
height: number; height: number;
}) => deps.updateVisibleOverlayBounds(geometry), }) => deps.updateVisibleOverlayBounds(geometry),
updateInvisibleOverlayBounds: (geometry: {
x: number;
y: number;
width: number;
height: number;
}) => deps.updateInvisibleOverlayBounds(geometry),
isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(), isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(),
isInvisibleOverlayVisible: () => deps.overlayManager.getInvisibleOverlayVisible(),
updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility: () =>
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(), deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
updateInvisibleOverlayVisibility: () =>
deps.overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
getOverlayWindows: () => deps.getOverlayWindows(), getOverlayWindows: () => deps.getOverlayWindows(),
syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(), syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(),
setWindowTracker: (tracker) => { setWindowTracker: (tracker) => {

View File

@@ -6,16 +6,11 @@ test('build initialize overlay runtime options maps dependencies', () => {
const calls: string[] = []; const calls: string[] = [];
const buildOptions = createBuildInitializeOverlayRuntimeOptionsHandler({ const buildOptions = createBuildInitializeOverlayRuntimeOptionsHandler({
getBackendOverride: () => 'x11', getBackendOverride: () => 'x11',
getInitialInvisibleOverlayVisibility: () => true,
createMainWindow: () => calls.push('create-main'), createMainWindow: () => calls.push('create-main'),
createInvisibleWindow: () => calls.push('create-invisible'),
registerGlobalShortcuts: () => calls.push('register-shortcuts'), registerGlobalShortcuts: () => calls.push('register-shortcuts'),
updateVisibleOverlayBounds: () => calls.push('update-visible-bounds'), updateVisibleOverlayBounds: () => calls.push('update-visible-bounds'),
updateInvisibleOverlayBounds: () => calls.push('update-invisible-bounds'),
isVisibleOverlayVisible: () => true, isVisibleOverlayVisible: () => true,
isInvisibleOverlayVisible: () => false,
updateVisibleOverlayVisibility: () => calls.push('update-visible'), updateVisibleOverlayVisibility: () => calls.push('update-visible'),
updateInvisibleOverlayVisibility: () => calls.push('update-invisible'),
getOverlayWindows: () => [], getOverlayWindows: () => [],
syncOverlayShortcuts: () => calls.push('sync-shortcuts'), syncOverlayShortcuts: () => calls.push('sync-shortcuts'),
setWindowTracker: () => calls.push('set-tracker'), setWindowTracker: () => calls.push('set-tracker'),
@@ -37,18 +32,13 @@ test('build initialize overlay runtime options maps dependencies', () => {
const options = buildOptions(); const options = buildOptions();
assert.equal(options.backendOverride, 'x11'); assert.equal(options.backendOverride, 'x11');
assert.equal(options.getInitialInvisibleOverlayVisibility(), true);
assert.equal(options.isVisibleOverlayVisible(), true); assert.equal(options.isVisibleOverlayVisible(), true);
assert.equal(options.isInvisibleOverlayVisible(), false);
assert.equal(options.getMpvSocketPath(), '/tmp/mpv.sock'); assert.equal(options.getMpvSocketPath(), '/tmp/mpv.sock');
assert.equal(options.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json'); assert.equal(options.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json');
options.createMainWindow(); options.createMainWindow();
options.createInvisibleWindow();
options.registerGlobalShortcuts(); options.registerGlobalShortcuts();
options.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); options.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
options.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
options.updateVisibleOverlayVisibility(); options.updateVisibleOverlayVisibility();
options.updateInvisibleOverlayVisibility();
options.syncOverlayShortcuts(); options.syncOverlayShortcuts();
options.setWindowTracker(null); options.setWindowTracker(null);
options.setAnkiIntegration(null); options.setAnkiIntegration(null);
@@ -56,12 +46,9 @@ test('build initialize overlay runtime options maps dependencies', () => {
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'create-main', 'create-main',
'create-invisible',
'register-shortcuts', 'register-shortcuts',
'update-visible-bounds', 'update-visible-bounds',
'update-invisible-bounds',
'update-visible', 'update-visible',
'update-invisible',
'sync-shortcuts', 'sync-shortcuts',
'set-tracker', 'set-tracker',
'set-anki', 'set-anki',

View File

@@ -9,16 +9,11 @@ import type { BaseWindowTracker } from '../../window-trackers';
type OverlayRuntimeOptions = { type OverlayRuntimeOptions = {
backendOverride: string | null; backendOverride: string | null;
getInitialInvisibleOverlayVisibility: () => boolean;
createMainWindow: () => void; createMainWindow: () => void;
createInvisibleWindow: () => void;
registerGlobalShortcuts: () => void; registerGlobalShortcuts: () => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
isInvisibleOverlayVisible: () => boolean;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
getOverlayWindows: () => BrowserWindow[]; getOverlayWindows: () => BrowserWindow[];
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
setWindowTracker: (tracker: BaseWindowTracker | null) => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void;
@@ -39,16 +34,11 @@ type OverlayRuntimeOptions = {
export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: { export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
getBackendOverride: () => string | null; getBackendOverride: () => string | null;
getInitialInvisibleOverlayVisibility: () => boolean;
createMainWindow: () => void; createMainWindow: () => void;
createInvisibleWindow: () => void;
registerGlobalShortcuts: () => void; registerGlobalShortcuts: () => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void; updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
isInvisibleOverlayVisible: () => boolean;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
getOverlayWindows: () => BrowserWindow[]; getOverlayWindows: () => BrowserWindow[];
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
setWindowTracker: (tracker: BaseWindowTracker | null) => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void;
@@ -68,16 +58,11 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
}) { }) {
return (): OverlayRuntimeOptions => ({ return (): OverlayRuntimeOptions => ({
backendOverride: deps.getBackendOverride(), backendOverride: deps.getBackendOverride(),
getInitialInvisibleOverlayVisibility: deps.getInitialInvisibleOverlayVisibility,
createMainWindow: deps.createMainWindow, createMainWindow: deps.createMainWindow,
createInvisibleWindow: deps.createInvisibleWindow,
registerGlobalShortcuts: deps.registerGlobalShortcuts, registerGlobalShortcuts: deps.registerGlobalShortcuts,
updateVisibleOverlayBounds: deps.updateVisibleOverlayBounds, updateVisibleOverlayBounds: deps.updateVisibleOverlayBounds,
updateInvisibleOverlayBounds: deps.updateInvisibleOverlayBounds,
isVisibleOverlayVisible: deps.isVisibleOverlayVisible, isVisibleOverlayVisible: deps.isVisibleOverlayVisible,
isInvisibleOverlayVisible: deps.isInvisibleOverlayVisible,
updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility, updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility,
updateInvisibleOverlayVisibility: deps.updateInvisibleOverlayVisibility,
getOverlayWindows: deps.getOverlayWindows, getOverlayWindows: deps.getOverlayWindows,
syncOverlayShortcuts: deps.syncOverlayShortcuts, syncOverlayShortcuts: deps.syncOverlayShortcuts,
setWindowTracker: deps.setWindowTracker, setWindowTracker: deps.setWindowTracker,

View File

@@ -1,9 +1,7 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { import {
createBuildSetInvisibleOverlayVisibleMainDepsHandler,
createBuildSetVisibleOverlayVisibleMainDepsHandler, createBuildSetVisibleOverlayVisibleMainDepsHandler,
createBuildToggleInvisibleOverlayMainDepsHandler,
createBuildToggleVisibleOverlayMainDepsHandler, createBuildToggleVisibleOverlayMainDepsHandler,
} from './overlay-visibility-actions-main-deps'; } from './overlay-visibility-actions-main-deps';
@@ -14,45 +12,14 @@ test('overlay visibility action main deps builders map callbacks', () => {
setVisibleOverlayVisibleCore: () => calls.push('visible-core'), setVisibleOverlayVisibleCore: () => calls.push('visible-core'),
setVisibleOverlayVisibleState: (visible) => calls.push(`visible-state:${visible}`), setVisibleOverlayVisibleState: (visible) => calls.push(`visible-state:${visible}`),
updateVisibleOverlayVisibility: () => calls.push('update-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({ setVisible.setVisibleOverlayVisibleCore({
visible: true, visible: true,
setVisibleOverlayVisibleState: () => {}, setVisibleOverlayVisibleState: () => {},
updateVisibleOverlayVisibility: () => {}, updateVisibleOverlayVisibility: () => {},
updateInvisibleOverlayVisibility: () => {},
syncInvisibleOverlayMousePassthrough: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isMpvConnected: () => true,
setMpvSubVisibility: () => {},
}); });
setVisible.setVisibleOverlayVisibleState(true); setVisible.setVisibleOverlayVisibleState(true);
setVisible.updateVisibleOverlayVisibility(); 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({ const toggleVisible = createBuildToggleVisibleOverlayMainDepsHandler({
getVisibleOverlayVisible: () => false, getVisibleOverlayVisible: () => false,
@@ -61,25 +28,10 @@ test('overlay visibility action main deps builders map callbacks', () => {
assert.equal(toggleVisible.getVisibleOverlayVisible(), false); assert.equal(toggleVisible.getVisibleOverlayVisible(), false);
toggleVisible.setVisibleOverlayVisible(true); 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, [ assert.deepEqual(calls, [
'visible-core', 'visible-core',
'visible-state:true', 'visible-state:true',
'update-visible', 'update-visible',
'update-invisible',
'sync',
'mpv:false',
'invisible-core',
'invisible-state:false',
'update-only-invisible',
'sync-only',
'toggle-visible:true', 'toggle-visible:true',
'toggle-invisible:false',
]); ]);
}); });

View File

@@ -1,14 +1,10 @@
import type { import type {
createSetInvisibleOverlayVisibleHandler,
createSetVisibleOverlayVisibleHandler, createSetVisibleOverlayVisibleHandler,
createToggleInvisibleOverlayHandler,
createToggleVisibleOverlayHandler, createToggleVisibleOverlayHandler,
} from './overlay-visibility-actions'; } from './overlay-visibility-actions';
type SetVisibleOverlayVisibleMainDeps = Parameters<typeof createSetVisibleOverlayVisibleHandler>[0]; type SetVisibleOverlayVisibleMainDeps = Parameters<typeof createSetVisibleOverlayVisibleHandler>[0];
type SetInvisibleOverlayVisibleMainDeps = Parameters<typeof createSetInvisibleOverlayVisibleHandler>[0];
type ToggleVisibleOverlayMainDeps = Parameters<typeof createToggleVisibleOverlayHandler>[0]; type ToggleVisibleOverlayMainDeps = Parameters<typeof createToggleVisibleOverlayHandler>[0];
type ToggleInvisibleOverlayMainDeps = Parameters<typeof createToggleInvisibleOverlayHandler>[0];
export function createBuildSetVisibleOverlayVisibleMainDepsHandler( export function createBuildSetVisibleOverlayVisibleMainDepsHandler(
deps: SetVisibleOverlayVisibleMainDeps, deps: SetVisibleOverlayVisibleMainDeps,
@@ -17,22 +13,6 @@ export function createBuildSetVisibleOverlayVisibleMainDepsHandler(
setVisibleOverlayVisibleCore: (options) => deps.setVisibleOverlayVisibleCore(options), setVisibleOverlayVisibleCore: (options) => deps.setVisibleOverlayVisibleCore(options),
setVisibleOverlayVisibleState: (visible: boolean) => deps.setVisibleOverlayVisibleState(visible), setVisibleOverlayVisibleState: (visible: boolean) => deps.setVisibleOverlayVisibleState(visible),
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), 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), setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
}); });
} }
export function createBuildToggleInvisibleOverlayMainDepsHandler(
deps: ToggleInvisibleOverlayMainDeps,
) {
return (): ToggleInvisibleOverlayMainDeps => ({
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
});
}

View File

@@ -1,9 +1,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
createSetInvisibleOverlayVisibleHandler,
createSetVisibleOverlayVisibleHandler, createSetVisibleOverlayVisibleHandler,
createToggleInvisibleOverlayHandler,
createToggleVisibleOverlayHandler, createToggleVisibleOverlayHandler,
} from './overlay-visibility-actions'; } from './overlay-visibility-actions';
@@ -14,17 +12,9 @@ test('set visible overlay handler forwards dependencies to core', () => {
calls.push(`core:${options.visible}`); calls.push(`core:${options.visible}`);
options.setVisibleOverlayVisibleState(options.visible); options.setVisibleOverlayVisibleState(options.visible);
options.updateVisibleOverlayVisibility(); options.updateVisibleOverlayVisibility();
options.updateInvisibleOverlayVisibility();
options.syncInvisibleOverlayMousePassthrough();
options.setMpvSubVisibility(!options.visible);
}, },
setVisibleOverlayVisibleState: (visible) => calls.push(`state:${visible}`), setVisibleOverlayVisibleState: (visible) => calls.push(`state:${visible}`),
updateVisibleOverlayVisibility: () => calls.push('update-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); setVisible(true);
@@ -32,30 +22,9 @@ test('set visible overlay handler forwards dependencies to core', () => {
'core:true', 'core:true',
'state:true', 'state:true',
'update-visible', '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', () => { test('toggle visible overlay flips current visible state', () => {
const calls: string[] = []; const calls: string[] = [];
let current = false; let current = false;
@@ -71,19 +40,3 @@ test('toggle visible overlay flips current visible state', () => {
toggle(); toggle();
assert.deepEqual(calls, ['set:true', 'set:false']); 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']);
});

View File

@@ -3,52 +3,15 @@ export function createSetVisibleOverlayVisibleHandler(deps: {
visible: boolean; visible: boolean;
setVisibleOverlayVisibleState: (visible: boolean) => void; setVisibleOverlayVisibleState: (visible: boolean) => void;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isMpvConnected: () => boolean;
setMpvSubVisibility: (visible: boolean) => void;
}) => void; }) => void;
setVisibleOverlayVisibleState: (visible: boolean) => void; setVisibleOverlayVisibleState: (visible: boolean) => void;
updateVisibleOverlayVisibility: () => void; updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isMpvConnected: () => boolean;
setMpvSubVisibility: (visible: boolean) => void;
}) { }) {
return (visible: boolean): void => { return (visible: boolean): void => {
deps.setVisibleOverlayVisibleCore({ deps.setVisibleOverlayVisibleCore({
visible, visible,
setVisibleOverlayVisibleState: deps.setVisibleOverlayVisibleState, setVisibleOverlayVisibleState: deps.setVisibleOverlayVisibleState,
updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility, 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()); deps.setVisibleOverlayVisible(!deps.getVisibleOverlayVisible());
}; };
} }
export function createToggleInvisibleOverlayHandler(deps: {
getInvisibleOverlayVisible: () => boolean;
setInvisibleOverlayVisible: (visible: boolean) => void;
}) {
return (): void => {
deps.setInvisibleOverlayVisible(!deps.getInvisibleOverlayVisible());
};
}

View File

@@ -8,14 +8,11 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
const calls: string[] = []; const calls: string[] = [];
let trackerNotReadyWarningShown = false; let trackerNotReadyWarningShown = false;
const mainWindow = { id: 'main' } as never; const mainWindow = { id: 'main' } as never;
const invisibleWindow = { id: 'invisible' } as never;
const tracker = { id: 'tracker' } as unknown as BaseWindowTracker; const tracker = { id: 'tracker' } as unknown as BaseWindowTracker;
const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({ const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({
getMainWindow: () => mainWindow, getMainWindow: () => mainWindow,
getInvisibleWindow: () => invisibleWindow,
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false,
getWindowTracker: () => tracker, getWindowTracker: () => tracker,
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown, getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
setTrackerNotReadyWarningShown: (shown) => { setTrackerNotReadyWarningShown: (shown) => {
@@ -23,30 +20,35 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
calls.push(`tracker-warning:${shown}`); calls.push(`tracker-warning:${shown}`);
}, },
updateVisibleOverlayBounds: () => calls.push('visible-bounds'), updateVisibleOverlayBounds: () => calls.push('visible-bounds'),
updateInvisibleOverlayBounds: () => calls.push('invisible-bounds'),
ensureOverlayWindowLevel: () => calls.push('ensure-level'), ensureOverlayWindowLevel: () => calls.push('ensure-level'),
syncPrimaryOverlayWindowLayer: (layer) => calls.push(`primary-layer:${layer}`),
enforceOverlayLayerOrder: () => calls.push('enforce-order'), enforceOverlayLayerOrder: () => calls.push('enforce-order'),
syncOverlayShortcuts: () => calls.push('sync-shortcuts'), 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.getMainWindow(), mainWindow);
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
assert.equal(deps.getVisibleOverlayVisible(), true); assert.equal(deps.getVisibleOverlayVisible(), true);
assert.equal(deps.getInvisibleOverlayVisible(), false);
assert.equal(deps.getTrackerNotReadyWarningShown(), false); assert.equal(deps.getTrackerNotReadyWarningShown(), false);
deps.setTrackerNotReadyWarningShown(true); deps.setTrackerNotReadyWarningShown(true);
deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 }); deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
deps.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
deps.ensureOverlayWindowLevel(mainWindow); deps.ensureOverlayWindowLevel(mainWindow);
deps.syncPrimaryOverlayWindowLayer('visible');
deps.enforceOverlayLayerOrder(); deps.enforceOverlayLayerOrder();
deps.syncOverlayShortcuts(); 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.equal(trackerNotReadyWarningShown, true);
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'tracker-warning:true', 'tracker-warning:true',
'visible-bounds', 'visible-bounds',
'invisible-bounds',
'ensure-level', 'ensure-level',
'primary-layer:visible',
'enforce-order', 'enforce-order',
'sync-shortcuts', 'sync-shortcuts',
'overlay-loading-osd',
]); ]);
}); });

View File

@@ -7,18 +7,19 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
) { ) {
return (): OverlayVisibilityRuntimeDeps => ({ return (): OverlayVisibilityRuntimeDeps => ({
getMainWindow: () => deps.getMainWindow(), getMainWindow: () => deps.getMainWindow(),
getInvisibleWindow: () => deps.getInvisibleWindow(),
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
getWindowTracker: () => deps.getWindowTracker(), getWindowTracker: () => deps.getWindowTracker(),
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(), getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown), setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
deps.updateVisibleOverlayBounds(geometry), deps.updateVisibleOverlayBounds(geometry),
updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
deps.updateInvisibleOverlayBounds(geometry),
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
syncPrimaryOverlayWindowLayer: (layer: 'visible') =>
deps.syncPrimaryOverlayWindowLayer(layer),
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(), enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(), syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
isMacOSPlatform: () => deps.isMacOSPlatform(),
showOverlayLoadingOsd: (message: string) => deps.showOverlayLoadingOsd(message),
resolveFallbackBounds: () => deps.resolveFallbackBounds(),
}); });
} }

View File

@@ -4,10 +4,7 @@ import { createOverlayVisibilityRuntime } from './overlay-visibility-runtime';
test('overlay visibility runtime wires set/toggle handlers through composed deps', () => { test('overlay visibility runtime wires set/toggle handlers through composed deps', () => {
let visible = false; let visible = false;
let invisible = true;
let setVisibleCoreCalls = 0; let setVisibleCoreCalls = 0;
let setInvisibleCoreCalls = 0;
let lastBoundSubVisibility: boolean | null = null;
const runtime = createOverlayVisibilityRuntime({ const runtime = createOverlayVisibilityRuntime({
setVisibleOverlayVisibleDeps: { setVisibleOverlayVisibleDeps: {
@@ -15,44 +12,17 @@ test('overlay visibility runtime wires set/toggle handlers through composed deps
setVisibleCoreCalls += 1; setVisibleCoreCalls += 1;
options.setVisibleOverlayVisibleState(options.visible); options.setVisibleOverlayVisibleState(options.visible);
options.updateVisibleOverlayVisibility(); options.updateVisibleOverlayVisibility();
options.updateInvisibleOverlayVisibility();
options.syncInvisibleOverlayMousePassthrough();
if (options.shouldBindVisibleOverlayToMpvSubVisibility() && options.isMpvConnected()) {
options.setMpvSubVisibility(options.visible);
}
}, },
setVisibleOverlayVisibleState: (nextVisible) => { setVisibleOverlayVisibleState: (nextVisible) => {
visible = nextVisible; visible = nextVisible;
}, },
updateVisibleOverlayVisibility: () => {}, 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, getVisibleOverlayVisible: () => visible,
getInvisibleOverlayVisible: () => invisible,
}); });
runtime.setVisibleOverlayVisible(true); runtime.setVisibleOverlayVisible(true);
assert.equal(visible, true); assert.equal(visible, true);
assert.equal(lastBoundSubVisibility, true);
runtime.toggleVisibleOverlay(); runtime.toggleVisibleOverlay();
assert.equal(visible, false); assert.equal(visible, false);
@@ -63,12 +33,5 @@ test('overlay visibility runtime wires set/toggle handlers through composed deps
runtime.toggleOverlay(); runtime.toggleOverlay();
assert.equal(visible, false); assert.equal(visible, false);
runtime.setInvisibleOverlayVisible(false);
assert.equal(invisible, false);
runtime.toggleInvisibleOverlay();
assert.equal(invisible, true);
assert.equal(setVisibleCoreCalls, 4); assert.equal(setVisibleCoreCalls, 4);
assert.equal(setInvisibleCoreCalls, 2);
}); });

View File

@@ -1,13 +1,9 @@
import { import {
createSetInvisibleOverlayVisibleHandler,
createSetVisibleOverlayVisibleHandler, createSetVisibleOverlayVisibleHandler,
createToggleInvisibleOverlayHandler,
createToggleVisibleOverlayHandler, createToggleVisibleOverlayHandler,
} from './overlay-visibility-actions'; } from './overlay-visibility-actions';
import { import {
createBuildSetInvisibleOverlayVisibleMainDepsHandler,
createBuildSetVisibleOverlayVisibleMainDepsHandler, createBuildSetVisibleOverlayVisibleMainDepsHandler,
createBuildToggleInvisibleOverlayMainDepsHandler,
createBuildToggleVisibleOverlayMainDepsHandler, createBuildToggleVisibleOverlayMainDepsHandler,
} from './overlay-visibility-actions-main-deps'; } from './overlay-visibility-actions-main-deps';
import { createSetOverlayVisibleHandler, createToggleOverlayHandler } from './overlay-main-actions'; import { createSetOverlayVisibleHandler, createToggleOverlayHandler } from './overlay-main-actions';
@@ -19,15 +15,10 @@ import {
type SetVisibleOverlayVisibleMainDeps = Parameters< type SetVisibleOverlayVisibleMainDeps = Parameters<
typeof createBuildSetVisibleOverlayVisibleMainDepsHandler typeof createBuildSetVisibleOverlayVisibleMainDepsHandler
>[0]; >[0];
type SetInvisibleOverlayVisibleMainDeps = Parameters<
typeof createBuildSetInvisibleOverlayVisibleMainDepsHandler
>[0];
export type OverlayVisibilityRuntimeDeps = { export type OverlayVisibilityRuntimeDeps = {
setVisibleOverlayVisibleDeps: SetVisibleOverlayVisibleMainDeps; setVisibleOverlayVisibleDeps: SetVisibleOverlayVisibleMainDeps;
setInvisibleOverlayVisibleDeps: SetInvisibleOverlayVisibleMainDeps;
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
}; };
export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDeps) { export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDeps) {
@@ -38,25 +29,12 @@ export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDep
setVisibleOverlayVisibleMainDeps, setVisibleOverlayVisibleMainDeps,
); );
const setInvisibleOverlayVisibleMainDeps = createBuildSetInvisibleOverlayVisibleMainDepsHandler(
deps.setInvisibleOverlayVisibleDeps,
)();
const setInvisibleOverlayVisible = createSetInvisibleOverlayVisibleHandler(
setInvisibleOverlayVisibleMainDeps,
);
const toggleVisibleOverlayMainDeps = createBuildToggleVisibleOverlayMainDepsHandler({ const toggleVisibleOverlayMainDeps = createBuildToggleVisibleOverlayMainDepsHandler({
getVisibleOverlayVisible: deps.getVisibleOverlayVisible, getVisibleOverlayVisible: deps.getVisibleOverlayVisible,
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
})(); })();
const toggleVisibleOverlay = createToggleVisibleOverlayHandler(toggleVisibleOverlayMainDeps); const toggleVisibleOverlay = createToggleVisibleOverlayHandler(toggleVisibleOverlayMainDeps);
const toggleInvisibleOverlayMainDeps = createBuildToggleInvisibleOverlayMainDepsHandler({
getInvisibleOverlayVisible: deps.getInvisibleOverlayVisible,
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
})();
const toggleInvisibleOverlay = createToggleInvisibleOverlayHandler(toggleInvisibleOverlayMainDeps);
const setOverlayVisibleMainDeps = createBuildSetOverlayVisibleMainDepsHandler({ const setOverlayVisibleMainDeps = createBuildSetOverlayVisibleMainDepsHandler({
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
})(); })();
@@ -69,9 +47,7 @@ export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDep
return { return {
setVisibleOverlayVisible, setVisibleOverlayVisible,
setInvisibleOverlayVisible,
toggleVisibleOverlay, toggleVisibleOverlay,
toggleInvisibleOverlay,
setOverlayVisible, setOverlayVisible,
toggleOverlay, toggleOverlay,
}; };

View File

@@ -3,7 +3,6 @@ import test from 'node:test';
import { import {
createBuildEnforceOverlayLayerOrderMainDepsHandler, createBuildEnforceOverlayLayerOrderMainDepsHandler,
createBuildEnsureOverlayWindowLevelMainDepsHandler, createBuildEnsureOverlayWindowLevelMainDepsHandler,
createBuildUpdateInvisibleOverlayBoundsMainDepsHandler,
createBuildUpdateVisibleOverlayBoundsMainDepsHandler, createBuildUpdateVisibleOverlayBoundsMainDepsHandler,
} from './overlay-window-layout-main-deps'; } from './overlay-window-layout-main-deps';
@@ -11,14 +10,9 @@ test('overlay window layout main deps builders map callbacks', () => {
const calls: string[] = []; const calls: string[] = [];
const visible = createBuildUpdateVisibleOverlayBoundsMainDepsHandler({ const visible = createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
setOverlayWindowBounds: (layer) => calls.push(`visible:${layer}`), setOverlayWindowBounds: () => calls.push('visible'),
})(); })();
visible.setOverlayWindowBounds('visible', { x: 0, y: 0, width: 1, height: 1 }); visible.setOverlayWindowBounds({ 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 });
const level = createBuildEnsureOverlayWindowLevelMainDepsHandler({ const level = createBuildEnsureOverlayWindowLevelMainDepsHandler({
ensureOverlayWindowLevelCore: () => calls.push('ensure'), ensureOverlayWindowLevelCore: () => calls.push('ensure'),
@@ -28,27 +22,20 @@ test('overlay window layout main deps builders map callbacks', () => {
const order = createBuildEnforceOverlayLayerOrderMainDepsHandler({ const order = createBuildEnforceOverlayLayerOrderMainDepsHandler({
enforceOverlayLayerOrderCore: () => calls.push('order'), enforceOverlayLayerOrderCore: () => calls.push('order'),
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false,
getMainWindow: () => ({ kind: 'main' }), getMainWindow: () => ({ kind: 'main' }),
getInvisibleWindow: () => ({ kind: 'invisible' }),
ensureOverlayWindowLevel: () => calls.push('order-level'), ensureOverlayWindowLevel: () => calls.push('order-level'),
})(); })();
order.enforceOverlayLayerOrderCore({ order.enforceOverlayLayerOrderCore({
visibleOverlayVisible: true, visibleOverlayVisible: true,
invisibleOverlayVisible: false,
mainWindow: null, mainWindow: null,
invisibleWindow: null,
ensureOverlayWindowLevel: () => {}, ensureOverlayWindowLevel: () => {},
}); });
assert.equal(order.getVisibleOverlayVisible(), true); assert.equal(order.getVisibleOverlayVisible(), true);
assert.equal(order.getInvisibleOverlayVisible(), false);
assert.deepEqual(order.getMainWindow(), { kind: 'main' }); assert.deepEqual(order.getMainWindow(), { kind: 'main' });
assert.deepEqual(order.getInvisibleWindow(), { kind: 'invisible' });
order.ensureOverlayWindowLevel({}); order.ensureOverlayWindowLevel({});
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'visible:visible', 'visible',
'invisible:invisible',
'ensure', 'ensure',
'order', 'order',
'order-level', 'order-level',

View File

@@ -1,12 +1,10 @@
import type { import type {
createEnforceOverlayLayerOrderHandler, createEnforceOverlayLayerOrderHandler,
createEnsureOverlayWindowLevelHandler, createEnsureOverlayWindowLevelHandler,
createUpdateInvisibleOverlayBoundsHandler,
createUpdateVisibleOverlayBoundsHandler, createUpdateVisibleOverlayBoundsHandler,
} from './overlay-window-layout'; } from './overlay-window-layout';
type UpdateVisibleOverlayBoundsMainDeps = Parameters<typeof createUpdateVisibleOverlayBoundsHandler>[0]; type UpdateVisibleOverlayBoundsMainDeps = Parameters<typeof createUpdateVisibleOverlayBoundsHandler>[0];
type UpdateInvisibleOverlayBoundsMainDeps = Parameters<typeof createUpdateInvisibleOverlayBoundsHandler>[0];
type EnsureOverlayWindowLevelMainDeps = Parameters<typeof createEnsureOverlayWindowLevelHandler>[0]; type EnsureOverlayWindowLevelMainDeps = Parameters<typeof createEnsureOverlayWindowLevelHandler>[0];
type EnforceOverlayLayerOrderMainDeps = Parameters<typeof createEnforceOverlayLayerOrderHandler>[0]; type EnforceOverlayLayerOrderMainDeps = Parameters<typeof createEnforceOverlayLayerOrderHandler>[0];
@@ -14,15 +12,7 @@ export function createBuildUpdateVisibleOverlayBoundsMainDepsHandler(
deps: UpdateVisibleOverlayBoundsMainDeps, deps: UpdateVisibleOverlayBoundsMainDeps,
) { ) {
return (): UpdateVisibleOverlayBoundsMainDeps => ({ return (): UpdateVisibleOverlayBoundsMainDeps => ({
setOverlayWindowBounds: (layer, geometry) => deps.setOverlayWindowBounds(layer, geometry), setOverlayWindowBounds: (geometry) => deps.setOverlayWindowBounds(geometry),
});
}
export function createBuildUpdateInvisibleOverlayBoundsMainDepsHandler(
deps: UpdateInvisibleOverlayBoundsMainDeps,
) {
return (): UpdateInvisibleOverlayBoundsMainDeps => ({
setOverlayWindowBounds: (layer, geometry) => deps.setOverlayWindowBounds(layer, geometry),
}); });
} }
@@ -40,9 +30,7 @@ export function createBuildEnforceOverlayLayerOrderMainDepsHandler(
return (): EnforceOverlayLayerOrderMainDeps => ({ return (): EnforceOverlayLayerOrderMainDeps => ({
enforceOverlayLayerOrderCore: (params) => deps.enforceOverlayLayerOrderCore(params), enforceOverlayLayerOrderCore: (params) => deps.enforceOverlayLayerOrderCore(params),
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
getMainWindow: () => deps.getMainWindow(), getMainWindow: () => deps.getMainWindow(),
getInvisibleWindow: () => deps.getInvisibleWindow(),
ensureOverlayWindowLevel: (window: unknown) => deps.ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window: unknown) => deps.ensureOverlayWindowLevel(window),
}); });
} }

View File

@@ -3,26 +3,17 @@ import assert from 'node:assert/strict';
import { import {
createEnforceOverlayLayerOrderHandler, createEnforceOverlayLayerOrderHandler,
createEnsureOverlayWindowLevelHandler, createEnsureOverlayWindowLevelHandler,
createUpdateInvisibleOverlayBoundsHandler,
createUpdateVisibleOverlayBoundsHandler, createUpdateVisibleOverlayBoundsHandler,
} from './overlay-window-layout'; } from './overlay-window-layout';
test('visible bounds handler writes visible layer geometry', () => { 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({ const handleVisible = createUpdateVisibleOverlayBoundsHandler({
setOverlayWindowBounds: (layer) => calls.push(layer), setOverlayWindowBounds: (geometry) => calls.push(geometry),
}); });
handleVisible({ x: 0, y: 0, width: 100, height: 50 }); const geometry = { x: 0, y: 0, width: 100, height: 50 };
assert.deepEqual(calls, ['visible']); handleVisible(geometry);
}); assert.deepEqual(calls, [geometry]);
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']);
}); });
test('ensure overlay window level handler delegates to core', () => { 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({ const enforce = createEnforceOverlayLayerOrderHandler({
enforceOverlayLayerOrderCore: (params) => { enforceOverlayLayerOrderCore: (params) => {
calls.push(params.visibleOverlayVisible ? 'visible-on' : 'visible-off'); calls.push(params.visibleOverlayVisible ? 'visible-on' : 'visible-off');
calls.push(params.invisibleOverlayVisible ? 'invisible-on' : 'invisible-off');
params.ensureOverlayWindowLevel({}); params.ensureOverlayWindowLevel({});
}, },
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false,
getMainWindow: () => ({}), getMainWindow: () => ({}),
getInvisibleWindow: () => ({}),
ensureOverlayWindowLevel: () => calls.push('ensure-level'), ensureOverlayWindowLevel: () => calls.push('ensure-level'),
}); });
enforce(); enforce();
assert.deepEqual(calls, ['visible-on', 'invisible-off', 'ensure-level']); assert.deepEqual(calls, ['visible-on', 'ensure-level']);
}); });

View File

@@ -1,18 +1,10 @@
import type { WindowGeometry } from '../../types'; import type { WindowGeometry } from '../../types';
export function createUpdateVisibleOverlayBoundsHandler(deps: { export function createUpdateVisibleOverlayBoundsHandler(deps: {
setOverlayWindowBounds: (layer: 'visible' | 'invisible', geometry: WindowGeometry) => void; setOverlayWindowBounds: (geometry: WindowGeometry) => void;
}) { }) {
return (geometry: WindowGeometry): void => { return (geometry: WindowGeometry): void => {
deps.setOverlayWindowBounds('visible', geometry); deps.setOverlayWindowBounds(geometry);
};
}
export function createUpdateInvisibleOverlayBoundsHandler(deps: {
setOverlayWindowBounds: (layer: 'visible' | 'invisible', geometry: WindowGeometry) => void;
}) {
return (geometry: WindowGeometry): void => {
deps.setOverlayWindowBounds('invisible', geometry);
}; };
} }
@@ -27,23 +19,17 @@ export function createEnsureOverlayWindowLevelHandler(deps: {
export function createEnforceOverlayLayerOrderHandler(deps: { export function createEnforceOverlayLayerOrderHandler(deps: {
enforceOverlayLayerOrderCore: (params: { enforceOverlayLayerOrderCore: (params: {
visibleOverlayVisible: boolean; visibleOverlayVisible: boolean;
invisibleOverlayVisible: boolean;
mainWindow: unknown; mainWindow: unknown;
invisibleWindow: unknown;
ensureOverlayWindowLevel: (window: unknown) => void; ensureOverlayWindowLevel: (window: unknown) => void;
}) => void; }) => void;
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
getMainWindow: () => unknown; getMainWindow: () => unknown;
getInvisibleWindow: () => unknown;
ensureOverlayWindowLevel: (window: unknown) => void; ensureOverlayWindowLevel: (window: unknown) => void;
}) { }) {
return (): void => { return (): void => {
deps.enforceOverlayLayerOrderCore({ deps.enforceOverlayLayerOrderCore({
visibleOverlayVisible: deps.getVisibleOverlayVisible(), visibleOverlayVisible: deps.getVisibleOverlayVisible(),
invisibleOverlayVisible: deps.getInvisibleOverlayVisible(),
mainWindow: deps.getMainWindow(), mainWindow: deps.getMainWindow(),
invisibleWindow: deps.getInvisibleWindow(),
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel, ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
}); });
}; };

View File

@@ -64,7 +64,6 @@ test('config derived runtime main deps builder maps callbacks', () => {
const deps = createBuildConfigDerivedRuntimeMainDepsHandler({ const deps = createBuildConfigDerivedRuntimeMainDepsHandler({
getResolvedConfig: () => ({ jimaku: {} } as never), getResolvedConfig: () => ({ jimaku: {} } as never),
getRuntimeOptionsManager: () => null, getRuntimeOptionsManager: () => null,
platform: 'darwin',
defaultJimakuLanguagePreference: 'ja', defaultJimakuLanguagePreference: 'ja',
defaultJimakuMaxEntryResults: 20, defaultJimakuMaxEntryResults: 20,
defaultJimakuApiBaseUrl: 'https://api.example.com', defaultJimakuApiBaseUrl: 'https://api.example.com',
@@ -72,7 +71,6 @@ test('config derived runtime main deps builder maps callbacks', () => {
assert.deepEqual(deps.getResolvedConfig(), { jimaku: {} }); assert.deepEqual(deps.getResolvedConfig(), { jimaku: {} });
assert.equal(deps.getRuntimeOptionsManager(), null); assert.equal(deps.getRuntimeOptionsManager(), null);
assert.equal(deps.platform, 'darwin');
assert.equal(deps.defaultJimakuLanguagePreference, 'ja'); assert.equal(deps.defaultJimakuLanguagePreference, 'ja');
assert.equal(deps.defaultJimakuMaxEntryResults, 20); assert.equal(deps.defaultJimakuMaxEntryResults, 20);
assert.equal(deps.defaultJimakuApiBaseUrl, 'https://api.example.com'); assert.equal(deps.defaultJimakuApiBaseUrl, 'https://api.example.com');

View File

@@ -35,7 +35,6 @@ export function createBuildConfigDerivedRuntimeMainDepsHandler(deps: ConfigDeriv
return (): ConfigDerivedRuntimeDeps => ({ return (): ConfigDerivedRuntimeDeps => ({
getResolvedConfig: () => deps.getResolvedConfig(), getResolvedConfig: () => deps.getResolvedConfig(),
getRuntimeOptionsManager: () => deps.getRuntimeOptionsManager(), getRuntimeOptionsManager: () => deps.getRuntimeOptionsManager(),
platform: deps.platform,
defaultJimakuLanguagePreference: deps.defaultJimakuLanguagePreference, defaultJimakuLanguagePreference: deps.defaultJimakuLanguagePreference,
defaultJimakuMaxEntryResults: deps.defaultJimakuMaxEntryResults, defaultJimakuMaxEntryResults: deps.defaultJimakuMaxEntryResults,
defaultJimakuApiBaseUrl: deps.defaultJimakuApiBaseUrl, defaultJimakuApiBaseUrl: deps.defaultJimakuApiBaseUrl,

View File

@@ -1,5 +1,6 @@
import type { Keybinding } from '../../types'; import type { Keybinding } from '../../types';
import type { RendererContext } from '../context'; import type { RendererContext } from '../context';
import { hasYomitanPopupIframe, isYomitanPopupIframe } from '../yomitan-popup.js';
export function createKeyboardHandlers( export function createKeyboardHandlers(
ctx: RendererContext, ctx: RendererContext,
@@ -14,11 +15,6 @@ export function createKeyboardHandlers(
fallbackUsed: boolean; fallbackUsed: boolean;
fallbackUnavailable: boolean; fallbackUnavailable: boolean;
}) => void; }) => void;
saveInvisiblePositionEdit: () => void;
cancelInvisiblePositionEdit: () => void;
setInvisiblePositionEditMode: (enabled: boolean) => void;
applyInvisibleSubtitleOffsetPosition: () => void;
updateInvisiblePositionEditHud: () => void;
appendClipboardVideoToQueue: () => void; appendClipboardVideoToQueue: () => void;
}, },
) { ) {
@@ -32,9 +28,6 @@ export function createKeyboardHandlers(
['KeyS', { type: 'mpv', command: ['script-message', 'subminer-start'] }], ['KeyS', { type: 'mpv', command: ['script-message', 'subminer-start'] }],
['Shift+KeyS', { type: 'mpv', command: ['script-message', 'subminer-stop'] }], ['Shift+KeyS', { type: 'mpv', command: ['script-message', 'subminer-stop'] }],
['KeyT', { type: 'mpv', command: ['script-message', 'subminer-toggle'] }], ['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'] }], ['KeyO', { type: 'mpv', command: ['script-message', 'subminer-options'] }],
['KeyR', { type: 'mpv', command: ['script-message', 'subminer-restart'] }], ['KeyR', { type: 'mpv', command: ['script-message', 'subminer-restart'] }],
['KeyC', { type: 'mpv', command: ['script-message', 'subminer-status'] }], ['KeyC', { type: 'mpv', command: ['script-message', 'subminer-status'] }],
@@ -46,10 +39,9 @@ export function createKeyboardHandlers(
if (!(target instanceof Element)) return false; if (!(target instanceof Element)) return false;
if (target.closest('.modal')) return true; if (target.closest('.modal')) return true;
if (ctx.dom.subtitleContainer.contains(target)) 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; return true;
}
if (target.closest && target.closest('iframe[id^="yomitan-popup"]')) return true;
return false; return false;
} }
@@ -63,15 +55,6 @@ export function createKeyboardHandlers(
return parts.join('+'); return parts.join('+');
} }
function isInvisiblePositionToggleShortcut(e: KeyboardEvent): boolean {
return (
e.code === ctx.platform.invisiblePositionEditToggleCode &&
!e.altKey &&
e.shiftKey &&
(e.ctrlKey || e.metaKey)
);
}
function resolveSessionHelpChordBinding(): { function resolveSessionHelpChordBinding(): {
bindingKey: 'KeyH' | 'KeyK'; bindingKey: 'KeyH' | 'KeyK';
fallbackUsed: boolean; 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 { function resetChord(): void {
ctx.state.chordPending = false; ctx.state.chordPending = false;
if (ctx.state.chordTimeout !== null) { if (ctx.state.chordTimeout !== null) {
@@ -188,9 +108,7 @@ export function createKeyboardHandlers(
updateKeybindings(await window.electronAPI.getKeybindings()); updateKeybindings(await window.electronAPI.getKeybindings());
document.addEventListener('keydown', (e: KeyboardEvent) => { document.addEventListener('keydown', (e: KeyboardEvent) => {
const yomitanPopup = document.querySelector('iframe[id^="yomitan-popup"]'); if (hasYomitanPopupIframe(document)) return;
if (yomitanPopup) return;
if (handleInvisiblePositionEditKeydown(e)) return;
if (ctx.state.runtimeOptionsModalOpen) { if (ctx.state.runtimeOptionsModalOpen) {
options.handleRuntimeOptionsKeydown(e); options.handleRuntimeOptionsKeydown(e);

Some files were not shown because too many files have changed in this diff Show More