mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-11 04:19:26 -07:00
Honor configured controller shortcuts and clean up modal opens
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
type: changed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Added configurable overlay shortcuts for session help, controller select, and controller debug actions.
|
||||||
|
- Added mpv/plugin and CLI routing for session help, controller utilities, and subtitle sidebar toggling through the shared session-action path.
|
||||||
|
- Improved dedicated overlay modal retry and focus handling for runtime options, Jimaku, session help, controller tools, and the playlist browser.
|
||||||
@@ -173,7 +173,11 @@
|
|||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||||
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
||||||
|
"openSessionHelp": "CommandOrControl+Shift+H", // Open session help setting.
|
||||||
|
"openControllerSelect": "Alt+C", // Open controller select setting.
|
||||||
|
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
||||||
|
"toggleSubtitleSidebar": "\\" // Toggle subtitle sidebar setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -536,7 +536,11 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
"mineSentenceMultiple": "CommandOrControl+Shift+S",
|
"mineSentenceMultiple": "CommandOrControl+Shift+S",
|
||||||
"markAudioCard": "CommandOrControl+Shift+A",
|
"markAudioCard": "CommandOrControl+Shift+A",
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O",
|
"openRuntimeOptions": "CommandOrControl+Shift+O",
|
||||||
|
"openSessionHelp": "CommandOrControl+Shift+H",
|
||||||
|
"openControllerSelect": "Alt+C",
|
||||||
|
"openControllerDebug": "Alt+Shift+C",
|
||||||
"openJimaku": "Ctrl+Shift+J",
|
"openJimaku": "Ctrl+Shift+J",
|
||||||
|
"toggleSubtitleSidebar": "\\",
|
||||||
"multiCopyTimeoutMs": 3000
|
"multiCopyTimeoutMs": 3000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,7 +560,11 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
|
| `toggleSecondarySub` | string \| `null` | Accelerator for cycling secondary subtitle mode (default: `"CommandOrControl+Shift+V"`) |
|
||||||
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
|
| `markAudioCard` | string \| `null` | Accelerator for marking last card as audio card (default: `"CommandOrControl+Shift+A"`) |
|
||||||
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
|
| `openRuntimeOptions` | string \| `null` | Opens runtime options palette for live session-only toggles (default: `"CommandOrControl+Shift+O"`) |
|
||||||
|
| `openSessionHelp` | string \| `null` | Opens the in-overlay session help modal (default: `"CommandOrControl+Shift+H"`) |
|
||||||
|
| `openControllerSelect` | string \| `null` | Opens the controller config/remap modal (default: `"Alt+C"`) |
|
||||||
|
| `openControllerDebug` | string \| `null` | Opens the controller debug modal (default: `"Alt+Shift+C"`) |
|
||||||
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
|
| `openJimaku` | string \| `null` | Opens the Jimaku search modal (default: `"Ctrl+Shift+J"`) |
|
||||||
|
| `toggleSubtitleSidebar` | string \| `null` | Dispatches the subtitle sidebar toggle action (default: `"\\"`). `subtitleSidebar.toggleKey` remains the primary bare-key setting. |
|
||||||
|
|
||||||
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
|
**See `config.example.jsonc`** for the complete list of shortcut configuration options.
|
||||||
|
|
||||||
@@ -573,9 +581,10 @@ Important behavior:
|
|||||||
- Controller input is only active while keyboard-only mode is enabled.
|
- Controller input is only active while keyboard-only mode is enabled.
|
||||||
- Keyboard-only mode continues to work normally without a controller.
|
- Keyboard-only mode continues to work normally without a controller.
|
||||||
- By default SubMiner uses the first connected controller.
|
- By default SubMiner uses the first connected controller.
|
||||||
- `Alt+C` opens the controller config modal, where you can save the selected controller and remap actions inline.
|
- `Alt+C` opens the controller config modal by default, and you can remap that shortcut through `shortcuts.openControllerSelect`.
|
||||||
- Click `Learn`, then press the next fresh button, trigger, or stick direction you want to bind for that overlay action.
|
- Click `Learn`, then press the next fresh button, trigger, or stick direction you want to bind for that overlay action.
|
||||||
- `Alt+Shift+C` opens a live debug modal showing raw axes/button values plus a ready-to-copy `buttonIndices` config block.
|
- `Alt+Shift+C` opens the debug modal by default, and you can remap that shortcut through `shortcuts.openControllerDebug`.
|
||||||
|
- The debug modal shows raw axes/button values plus a ready-to-copy `buttonIndices` config block.
|
||||||
- `controller.buttonIndices` is a semantic reference/legacy mapping. Changing it does not rewrite the raw numeric descriptor values already stored under `controller.bindings`.
|
- `controller.buttonIndices` is a semantic reference/legacy mapping. Changing it does not rewrite the raw numeric descriptor values already stored under `controller.bindings`.
|
||||||
- Turning keyboard-only mode off clears the keyboard-only token highlight state.
|
- Turning keyboard-only mode off clears the keyboard-only token highlight state.
|
||||||
- Closing the Yomitan popup clears the temporary native text-selection fill, but keeps controller token selection active.
|
- Closing the Yomitan popup clears the temporary native text-selection fill, but keeps controller token selection active.
|
||||||
@@ -694,7 +703,7 @@ These shortcuts are only active when the overlay window is visible and automatic
|
|||||||
|
|
||||||
### Session Help Modal
|
### Session Help Modal
|
||||||
|
|
||||||
The session help modal is opened with `Y-H` by default (falls back to `Y-K` if needed) and shows the current session keybindings and color legend.
|
The session help modal opens from the overlay with `Ctrl/Cmd+Shift+H` by default. The mpv plugin also exposes it through the `Y-H` chord (falling back to `Y-K` if needed). It shows the current session keybindings and color legend.
|
||||||
|
|
||||||
You can filter the modal quickly with `/`:
|
You can filter the modal quickly with `/`:
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,11 @@
|
|||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||||
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
||||||
|
"openSessionHelp": "CommandOrControl+Shift+H", // Open session help setting.
|
||||||
|
"openControllerSelect": "Alt+C", // Open controller select setting.
|
||||||
|
"openControllerDebug": "Alt+Shift+C", // Open controller debug setting.
|
||||||
|
"toggleSubtitleSidebar": "\\" // Toggle subtitle sidebar setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
|
|||||||
| ------------------ | -------------------------------------------------------- | ------------------------------ |
|
| ------------------ | -------------------------------------------------------- | ------------------------------ |
|
||||||
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
| `Ctrl/Cmd+Shift+V` | Cycle secondary subtitle mode (hidden → visible → hover) | `shortcuts.toggleSecondarySub` |
|
||||||
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
||||||
|
| `Ctrl/Cmd+Shift+H` | Open session help modal | `shortcuts.openSessionHelp` |
|
||||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||||
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` |
|
| `Ctrl+Alt+C` | Open the manual YouTube subtitle picker | `keybindings` |
|
||||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||||
@@ -79,12 +80,12 @@ The subtitle sidebar toggle is overlay-local and only opens when SubMiner has a
|
|||||||
|
|
||||||
## Controller Shortcuts
|
## Controller Shortcuts
|
||||||
|
|
||||||
These overlay-local shortcuts are fixed and open controller utilities for the Chrome Gamepad API integration.
|
These overlay-local shortcuts open controller utilities for the Chrome Gamepad API integration.
|
||||||
|
|
||||||
| Shortcut | Action | Configurable |
|
| Shortcut | Action | Configurable |
|
||||||
| ------------- | ------------------------------ | ------------ |
|
| ------------- | ------------------------------------ | -------------------------------- |
|
||||||
| `Alt+C` | Open controller config + remap modal | Fixed |
|
| `Alt+C` | Open controller config + remap modal | `shortcuts.openControllerSelect` |
|
||||||
| `Alt+Shift+C` | Open controller debug modal | Fixed |
|
| `Alt+Shift+C` | Open controller debug modal | `shortcuts.openControllerDebug` |
|
||||||
|
|
||||||
Controller input only drives the overlay while keyboard-only mode is enabled. The controller mapping and tuning live under the top-level `controller` config block; keyboard-only mode still works normally without a controller.
|
Controller input only drives the overlay while keyboard-only mode is enabled. The controller mapping and tuning live under the top-level `controller` config block; keyboard-only mode still works normally without a controller.
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ When the mpv plugin is installed, all commands use a `y` chord prefix — press
|
|||||||
| `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 |
|
||||||
|
| `y-h` | Open session help |
|
||||||
|
|
||||||
When the overlay has focus, press `y` then `d` to toggle DevTools (debugging helper).
|
When the overlay has focus, press `y` then `d` to toggle DevTools (debugging helper).
|
||||||
|
|
||||||
|
|||||||
@@ -272,12 +272,12 @@ SubMiner supports gamepad/controller input for couch-friendly usage via the Chro
|
|||||||
|
|
||||||
1. Connect a controller before or after launching SubMiner.
|
1. Connect a controller before or after launching SubMiner.
|
||||||
2. Enable keyboard-only mode — press `Y` on the controller (default binding) or use the overlay keybinding.
|
2. Enable keyboard-only mode — press `Y` on the controller (default binding) or use the overlay keybinding.
|
||||||
3. Press `Alt+C` in the overlay to pick the controller you want to save and remap any action inline.
|
3. Press `Alt+C` in the overlay by default to pick the controller you want to save and remap any action inline.
|
||||||
4. Click `Learn` on the overlay action you want, then press the matching button, trigger, or stick direction on the controller.
|
4. Click `Learn` on the overlay action you want, then press the matching button, trigger, or stick direction on the controller.
|
||||||
5. Use the left stick to navigate subtitle tokens and scroll the popup; use the right stick vertically for popup page jumps.
|
5. Use the left stick to navigate subtitle tokens and scroll the popup; use the right stick vertically for popup page jumps.
|
||||||
6. Press `A` to look up the selected word, `X` to mine a card, `B` to close the popup.
|
6. Press `A` to look up the selected word, `X` to mine a card, `B` to close the popup.
|
||||||
|
|
||||||
By default SubMiner uses the first connected controller. `Alt+C` opens the controller config modal, where you can save the preferred controller and remap bindings inline. `Alt+Shift+C` still opens the live debug modal with raw axes/button values for non-standard pads.
|
By default SubMiner uses the first connected controller. `Alt+C` opens the controller config modal, where you can save the preferred controller and remap bindings inline, and `Alt+Shift+C` opens the live debug modal with raw axes/button values for non-standard pads. Both shortcuts can be changed through `shortcuts.openControllerSelect` and `shortcuts.openControllerDebug`.
|
||||||
|
|
||||||
### Default Button Mapping
|
### Default Button Mapping
|
||||||
|
|
||||||
@@ -321,6 +321,8 @@ See [Keyboard Shortcuts](/shortcuts) for the full reference, including mining sh
|
|||||||
|
|
||||||
Useful overlay-local default keybinding: `Ctrl+Alt+P` opens the playlist browser for the current video's parent directory and the live mpv queue so you can append, reorder, remove, or jump between episodes without leaving playback.
|
Useful overlay-local default keybinding: `Ctrl+Alt+P` opens the playlist browser for the current video's parent directory and the live mpv queue so you can append, reorder, remove, or jump between episodes without leaving playback.
|
||||||
|
|
||||||
|
`Ctrl/Cmd+Shift+H` opens the session help modal with the current overlay and mpv keybindings. If you use the mpv plugin, the same help view is also available through the `y-h` chord.
|
||||||
|
|
||||||
Hovering over subtitle text pauses mpv by default; leaving resumes it. Yomitan popups also pause playback by default. Set `subtitleStyle.autoPauseVideoOnHover: false` or `subtitleStyle.autoPauseVideoOnYomitanPopup: false` to disable either behavior.
|
Hovering over subtitle text pauses mpv by default; leaving resumes it. Yomitan popups also pause playback by default. Set `subtitleStyle.autoPauseVideoOnHover: false` or `subtitleStyle.autoPauseVideoOnYomitanPopup: false` to disable either behavior.
|
||||||
|
|
||||||
### Drag-and-Drop
|
### Drag-and-Drop
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ function M.create(ctx)
|
|||||||
return { "--mine-sentence-count", tostring(payload and payload.count or 1) }
|
return { "--mine-sentence-count", tostring(payload and payload.count or 1) }
|
||||||
elseif action_id == "toggleSecondarySub" then
|
elseif action_id == "toggleSecondarySub" then
|
||||||
return { "--toggle-secondary-sub" }
|
return { "--toggle-secondary-sub" }
|
||||||
|
elseif action_id == "toggleSubtitleSidebar" then
|
||||||
|
return { "--toggle-subtitle-sidebar" }
|
||||||
elseif action_id == "markAudioCard" then
|
elseif action_id == "markAudioCard" then
|
||||||
return { "--mark-audio-card" }
|
return { "--mark-audio-card" }
|
||||||
elseif action_id == "openRuntimeOptions" then
|
elseif action_id == "openRuntimeOptions" then
|
||||||
@@ -141,6 +143,12 @@ function M.create(ctx)
|
|||||||
return { "--open-jimaku" }
|
return { "--open-jimaku" }
|
||||||
elseif action_id == "openYoutubePicker" then
|
elseif action_id == "openYoutubePicker" then
|
||||||
return { "--open-youtube-picker" }
|
return { "--open-youtube-picker" }
|
||||||
|
elseif action_id == "openSessionHelp" then
|
||||||
|
return { "--open-session-help" }
|
||||||
|
elseif action_id == "openControllerSelect" then
|
||||||
|
return { "--open-controller-select" }
|
||||||
|
elseif action_id == "openControllerDebug" then
|
||||||
|
return { "--open-controller-debug" }
|
||||||
elseif action_id == "openPlaylistBrowser" then
|
elseif action_id == "openPlaylistBrowser" then
|
||||||
return { "--open-playlist-browser" }
|
return { "--open-playlist-browser" }
|
||||||
elseif action_id == "replayCurrentSubtitle" then
|
elseif action_id == "replayCurrentSubtitle" then
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ function M.create(ctx)
|
|||||||
mp.add_key_binding("y-c", "subminer-status", function()
|
mp.add_key_binding("y-c", "subminer-status", function()
|
||||||
process.check_status()
|
process.check_status()
|
||||||
end)
|
end)
|
||||||
|
mp.add_key_binding("y-h", "subminer-session-help", function()
|
||||||
|
if not ensure_binary_for_menu() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
process.run_control_command_async("open-session-help")
|
||||||
|
end)
|
||||||
if type(opts.aniskip_button_key) == "string" and opts.aniskip_button_key ~= "" then
|
if type(opts.aniskip_button_key) == "string" and opts.aniskip_button_key ~= "" then
|
||||||
mp.add_key_binding(opts.aniskip_button_key, "subminer-skip-intro", function()
|
mp.add_key_binding(opts.aniskip_button_key, "subminer-skip-intro", function()
|
||||||
aniskip.skip_intro_now()
|
aniskip.skip_intro_now()
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ export interface CliArgs {
|
|||||||
triggerSubsync: boolean;
|
triggerSubsync: boolean;
|
||||||
markAudioCard: boolean;
|
markAudioCard: boolean;
|
||||||
toggleStatsOverlay: boolean;
|
toggleStatsOverlay: boolean;
|
||||||
|
toggleSubtitleSidebar: boolean;
|
||||||
openRuntimeOptions: boolean;
|
openRuntimeOptions: boolean;
|
||||||
|
openSessionHelp: boolean;
|
||||||
|
openControllerSelect: boolean;
|
||||||
|
openControllerDebug: boolean;
|
||||||
openJimaku: boolean;
|
openJimaku: boolean;
|
||||||
openYoutubePicker: boolean;
|
openYoutubePicker: boolean;
|
||||||
openPlaylistBrowser: boolean;
|
openPlaylistBrowser: boolean;
|
||||||
@@ -115,7 +119,11 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
toggleStatsOverlay: false,
|
toggleStatsOverlay: false,
|
||||||
|
toggleSubtitleSidebar: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
|
openSessionHelp: false,
|
||||||
|
openControllerSelect: false,
|
||||||
|
openControllerDebug: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
openPlaylistBrowser: false,
|
openPlaylistBrowser: false,
|
||||||
@@ -218,7 +226,11 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
else if (arg === '--trigger-subsync') args.triggerSubsync = true;
|
else if (arg === '--trigger-subsync') args.triggerSubsync = true;
|
||||||
else if (arg === '--mark-audio-card') args.markAudioCard = true;
|
else if (arg === '--mark-audio-card') args.markAudioCard = true;
|
||||||
else if (arg === '--toggle-stats-overlay') args.toggleStatsOverlay = true;
|
else if (arg === '--toggle-stats-overlay') args.toggleStatsOverlay = true;
|
||||||
|
else if (arg === '--toggle-subtitle-sidebar') args.toggleSubtitleSidebar = true;
|
||||||
else if (arg === '--open-runtime-options') args.openRuntimeOptions = true;
|
else if (arg === '--open-runtime-options') args.openRuntimeOptions = true;
|
||||||
|
else if (arg === '--open-session-help') args.openSessionHelp = true;
|
||||||
|
else if (arg === '--open-controller-select') args.openControllerSelect = true;
|
||||||
|
else if (arg === '--open-controller-debug') args.openControllerDebug = true;
|
||||||
else if (arg === '--open-jimaku') args.openJimaku = true;
|
else if (arg === '--open-jimaku') args.openJimaku = true;
|
||||||
else if (arg === '--open-youtube-picker') args.openYoutubePicker = true;
|
else if (arg === '--open-youtube-picker') args.openYoutubePicker = true;
|
||||||
else if (arg === '--open-playlist-browser') args.openPlaylistBrowser = true;
|
else if (arg === '--open-playlist-browser') args.openPlaylistBrowser = true;
|
||||||
@@ -442,7 +454,11 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
|||||||
args.triggerSubsync ||
|
args.triggerSubsync ||
|
||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
args.toggleStatsOverlay ||
|
args.toggleStatsOverlay ||
|
||||||
|
args.toggleSubtitleSidebar ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
|
args.openSessionHelp ||
|
||||||
|
args.openControllerSelect ||
|
||||||
|
args.openControllerDebug ||
|
||||||
args.openJimaku ||
|
args.openJimaku ||
|
||||||
args.openYoutubePicker ||
|
args.openYoutubePicker ||
|
||||||
args.openPlaylistBrowser ||
|
args.openPlaylistBrowser ||
|
||||||
@@ -505,7 +521,11 @@ export function isStandaloneTexthookerCommand(args: CliArgs): boolean {
|
|||||||
!args.triggerSubsync &&
|
!args.triggerSubsync &&
|
||||||
!args.markAudioCard &&
|
!args.markAudioCard &&
|
||||||
!args.toggleStatsOverlay &&
|
!args.toggleStatsOverlay &&
|
||||||
|
!args.toggleSubtitleSidebar &&
|
||||||
!args.openRuntimeOptions &&
|
!args.openRuntimeOptions &&
|
||||||
|
!args.openSessionHelp &&
|
||||||
|
!args.openControllerSelect &&
|
||||||
|
!args.openControllerDebug &&
|
||||||
!args.openJimaku &&
|
!args.openJimaku &&
|
||||||
!args.openYoutubePicker &&
|
!args.openYoutubePicker &&
|
||||||
!args.openPlaylistBrowser &&
|
!args.openPlaylistBrowser &&
|
||||||
@@ -559,7 +579,11 @@ export function shouldStartApp(args: CliArgs): boolean {
|
|||||||
args.triggerSubsync ||
|
args.triggerSubsync ||
|
||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
args.toggleStatsOverlay ||
|
args.toggleStatsOverlay ||
|
||||||
|
args.toggleSubtitleSidebar ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
|
args.openSessionHelp ||
|
||||||
|
args.openControllerSelect ||
|
||||||
|
args.openControllerDebug ||
|
||||||
args.openJimaku ||
|
args.openJimaku ||
|
||||||
args.openYoutubePicker ||
|
args.openYoutubePicker ||
|
||||||
args.openPlaylistBrowser ||
|
args.openPlaylistBrowser ||
|
||||||
@@ -608,7 +632,11 @@ export function shouldRunSettingsOnlyStartup(args: CliArgs): boolean {
|
|||||||
!args.triggerSubsync &&
|
!args.triggerSubsync &&
|
||||||
!args.markAudioCard &&
|
!args.markAudioCard &&
|
||||||
!args.toggleStatsOverlay &&
|
!args.toggleStatsOverlay &&
|
||||||
|
!args.toggleSubtitleSidebar &&
|
||||||
!args.openRuntimeOptions &&
|
!args.openRuntimeOptions &&
|
||||||
|
!args.openSessionHelp &&
|
||||||
|
!args.openControllerSelect &&
|
||||||
|
!args.openControllerDebug &&
|
||||||
!args.openJimaku &&
|
!args.openJimaku &&
|
||||||
!args.openYoutubePicker &&
|
!args.openYoutubePicker &&
|
||||||
!args.openPlaylistBrowser &&
|
!args.openPlaylistBrowser &&
|
||||||
@@ -658,10 +686,14 @@ export function commandNeedsOverlayRuntime(args: CliArgs): boolean {
|
|||||||
args.mineSentenceMultiple ||
|
args.mineSentenceMultiple ||
|
||||||
args.updateLastCardFromClipboard ||
|
args.updateLastCardFromClipboard ||
|
||||||
args.toggleSecondarySub ||
|
args.toggleSecondarySub ||
|
||||||
|
args.toggleSubtitleSidebar ||
|
||||||
args.triggerFieldGrouping ||
|
args.triggerFieldGrouping ||
|
||||||
args.triggerSubsync ||
|
args.triggerSubsync ||
|
||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
|
args.openSessionHelp ||
|
||||||
|
args.openControllerSelect ||
|
||||||
|
args.openControllerDebug ||
|
||||||
args.openJimaku ||
|
args.openJimaku ||
|
||||||
args.openYoutubePicker ||
|
args.openYoutubePicker ||
|
||||||
args.openPlaylistBrowser ||
|
args.openPlaylistBrowser ||
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ ${B}Mining${R}
|
|||||||
--trigger-field-grouping Run Kiku field grouping
|
--trigger-field-grouping Run Kiku field grouping
|
||||||
--trigger-subsync Run subtitle sync
|
--trigger-subsync Run subtitle sync
|
||||||
--toggle-secondary-sub Cycle secondary subtitle mode
|
--toggle-secondary-sub Cycle secondary subtitle mode
|
||||||
|
--toggle-subtitle-sidebar Toggle subtitle sidebar panel
|
||||||
--open-runtime-options Open runtime options palette
|
--open-runtime-options Open runtime options palette
|
||||||
|
--open-session-help Open session help modal
|
||||||
|
--open-controller-select Open controller select modal
|
||||||
|
--open-controller-debug Open controller debug modal
|
||||||
|
|
||||||
${B}AniList${R}
|
${B}AniList${R}
|
||||||
--anilist-setup Open AniList authentication flow
|
--anilist-setup Open AniList authentication flow
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ export const CORE_DEFAULT_CONFIG: Pick<
|
|||||||
markAudioCard: 'CommandOrControl+Shift+A',
|
markAudioCard: 'CommandOrControl+Shift+A',
|
||||||
openRuntimeOptions: 'CommandOrControl+Shift+O',
|
openRuntimeOptions: 'CommandOrControl+Shift+O',
|
||||||
openJimaku: 'Ctrl+Shift+J',
|
openJimaku: 'Ctrl+Shift+J',
|
||||||
|
openSessionHelp: 'CommandOrControl+Shift+H',
|
||||||
|
openControllerSelect: 'Alt+C',
|
||||||
|
openControllerDebug: 'Alt+Shift+C',
|
||||||
|
toggleSubtitleSidebar: '\\',
|
||||||
},
|
},
|
||||||
secondarySub: {
|
secondarySub: {
|
||||||
secondarySubLanguages: [],
|
secondarySubLanguages: [],
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
toggleStatsOverlay: false,
|
toggleStatsOverlay: false,
|
||||||
|
toggleSubtitleSidebar: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
|
openSessionHelp: false,
|
||||||
|
openControllerSelect: false,
|
||||||
|
openControllerDebug: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
openPlaylistBrowser: false,
|
openPlaylistBrowser: false,
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
toggleStatsOverlay: false,
|
toggleStatsOverlay: false,
|
||||||
|
toggleSubtitleSidebar: false,
|
||||||
refreshKnownWords: false,
|
refreshKnownWords: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
|
openSessionHelp: false,
|
||||||
|
openControllerSelect: false,
|
||||||
|
openControllerDebug: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
openPlaylistBrowser: false,
|
openPlaylistBrowser: false,
|
||||||
|
|||||||
@@ -402,8 +402,32 @@ export function handleCliCommand(
|
|||||||
'toggleStatsOverlay',
|
'toggleStatsOverlay',
|
||||||
'Stats toggle failed',
|
'Stats toggle failed',
|
||||||
);
|
);
|
||||||
|
} else if (args.toggleSubtitleSidebar) {
|
||||||
|
dispatchCliSessionAction(
|
||||||
|
{ actionId: 'toggleSubtitleSidebar' },
|
||||||
|
'toggleSubtitleSidebar',
|
||||||
|
'Subtitle sidebar toggle failed',
|
||||||
|
);
|
||||||
} else if (args.openRuntimeOptions) {
|
} else if (args.openRuntimeOptions) {
|
||||||
deps.openRuntimeOptionsPalette();
|
deps.openRuntimeOptionsPalette();
|
||||||
|
} else if (args.openSessionHelp) {
|
||||||
|
dispatchCliSessionAction(
|
||||||
|
{ actionId: 'openSessionHelp' },
|
||||||
|
'openSessionHelp',
|
||||||
|
'Open session help failed',
|
||||||
|
);
|
||||||
|
} else if (args.openControllerSelect) {
|
||||||
|
dispatchCliSessionAction(
|
||||||
|
{ actionId: 'openControllerSelect' },
|
||||||
|
'openControllerSelect',
|
||||||
|
'Open controller select failed',
|
||||||
|
);
|
||||||
|
} else if (args.openControllerDebug) {
|
||||||
|
dispatchCliSessionAction(
|
||||||
|
{ actionId: 'openControllerDebug' },
|
||||||
|
'openControllerDebug',
|
||||||
|
'Open controller debug failed',
|
||||||
|
);
|
||||||
} else if (args.openJimaku) {
|
} else if (args.openJimaku) {
|
||||||
dispatchCliSessionAction({ actionId: 'openJimaku' }, 'openJimaku', 'Open jimaku failed');
|
dispatchCliSessionAction({ actionId: 'openJimaku' }, 'openJimaku', 'Open jimaku failed');
|
||||||
} else if (args.openYoutubePicker) {
|
} else if (args.openYoutubePicker) {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ function makeShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configured
|
|||||||
markAudioCard: null,
|
markAudioCard: null,
|
||||||
openRuntimeOptions: null,
|
openRuntimeOptions: null,
|
||||||
openJimaku: null,
|
openJimaku: null,
|
||||||
|
openSessionHelp: null,
|
||||||
|
openControllerSelect: null,
|
||||||
|
openControllerDebug: null,
|
||||||
|
toggleSubtitleSidebar: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ function createShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configur
|
|||||||
markAudioCard: null,
|
markAudioCard: null,
|
||||||
openRuntimeOptions: null,
|
openRuntimeOptions: null,
|
||||||
openJimaku: null,
|
openJimaku: null,
|
||||||
|
openSessionHelp: null,
|
||||||
|
openControllerSelect: null,
|
||||||
|
openControllerDebug: null,
|
||||||
|
toggleSubtitleSidebar: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export interface SessionActionExecutorDeps {
|
|||||||
mineSentenceCard: () => Promise<void>;
|
mineSentenceCard: () => Promise<void>;
|
||||||
mineSentenceCount: (count: number) => void;
|
mineSentenceCount: (count: number) => void;
|
||||||
toggleSecondarySub: () => void;
|
toggleSecondarySub: () => void;
|
||||||
|
toggleSubtitleSidebar: () => void;
|
||||||
markLastCardAsAudioCard: () => Promise<void>;
|
markLastCardAsAudioCard: () => Promise<void>;
|
||||||
openRuntimeOptionsPalette: () => void;
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
openSessionHelp: () => void;
|
||||||
|
openControllerSelect: () => void;
|
||||||
|
openControllerDebug: () => void;
|
||||||
openJimaku: () => void;
|
openJimaku: () => void;
|
||||||
openYoutubeTrackPicker: () => void | Promise<void>;
|
openYoutubeTrackPicker: () => void | Promise<void>;
|
||||||
openPlaylistBrowser: () => boolean | void | Promise<boolean | void>;
|
openPlaylistBrowser: () => boolean | void | Promise<boolean | void>;
|
||||||
@@ -65,12 +69,24 @@ export async function dispatchSessionAction(
|
|||||||
case 'toggleSecondarySub':
|
case 'toggleSecondarySub':
|
||||||
deps.toggleSecondarySub();
|
deps.toggleSecondarySub();
|
||||||
return;
|
return;
|
||||||
|
case 'toggleSubtitleSidebar':
|
||||||
|
deps.toggleSubtitleSidebar();
|
||||||
|
return;
|
||||||
case 'markAudioCard':
|
case 'markAudioCard':
|
||||||
await deps.markLastCardAsAudioCard();
|
await deps.markLastCardAsAudioCard();
|
||||||
return;
|
return;
|
||||||
case 'openRuntimeOptions':
|
case 'openRuntimeOptions':
|
||||||
deps.openRuntimeOptionsPalette();
|
deps.openRuntimeOptionsPalette();
|
||||||
return;
|
return;
|
||||||
|
case 'openSessionHelp':
|
||||||
|
deps.openSessionHelp();
|
||||||
|
return;
|
||||||
|
case 'openControllerSelect':
|
||||||
|
deps.openControllerSelect();
|
||||||
|
return;
|
||||||
|
case 'openControllerDebug':
|
||||||
|
deps.openControllerDebug();
|
||||||
|
return;
|
||||||
case 'openJimaku':
|
case 'openJimaku':
|
||||||
deps.openJimaku();
|
deps.openJimaku();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ function createShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configur
|
|||||||
markAudioCard: null,
|
markAudioCard: null,
|
||||||
openRuntimeOptions: null,
|
openRuntimeOptions: null,
|
||||||
openJimaku: null,
|
openJimaku: null,
|
||||||
|
openSessionHelp: null,
|
||||||
|
openControllerSelect: null,
|
||||||
|
openControllerDebug: null,
|
||||||
|
toggleSubtitleSidebar: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -33,6 +37,7 @@ test('compileSessionBindings merges shortcuts and keybindings into one canonical
|
|||||||
shortcuts: createShortcuts({
|
shortcuts: createShortcuts({
|
||||||
toggleVisibleOverlayGlobal: 'Alt+Shift+O',
|
toggleVisibleOverlayGlobal: 'Alt+Shift+O',
|
||||||
openJimaku: 'Ctrl+Shift+J',
|
openJimaku: 'Ctrl+Shift+J',
|
||||||
|
openControllerSelect: 'Alt+C',
|
||||||
}),
|
}),
|
||||||
keybindings: [
|
keybindings: [
|
||||||
createKeybinding('KeyF', ['cycle', 'fullscreen']),
|
createKeybinding('KeyF', ['cycle', 'fullscreen']),
|
||||||
@@ -68,6 +73,13 @@ test('compileSessionBindings merges shortcuts and keybindings into one canonical
|
|||||||
modifiers: ['ctrl', 'shift'],
|
modifiers: ['ctrl', 'shift'],
|
||||||
target: 'openYoutubePicker',
|
target: 'openYoutubePicker',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
actionType: 'session-action',
|
||||||
|
sourcePath: 'shortcuts.openControllerSelect',
|
||||||
|
code: 'KeyC',
|
||||||
|
modifiers: ['alt'],
|
||||||
|
target: 'openControllerSelect',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
actionType: 'session-action',
|
actionType: 'session-action',
|
||||||
sourcePath: 'shortcuts.openJimaku',
|
sourcePath: 'shortcuts.openJimaku',
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ const SESSION_SHORTCUT_ACTIONS: Array<{
|
|||||||
{ key: 'markAudioCard', actionId: 'markAudioCard' },
|
{ key: 'markAudioCard', actionId: 'markAudioCard' },
|
||||||
{ key: 'openRuntimeOptions', actionId: 'openRuntimeOptions' },
|
{ key: 'openRuntimeOptions', actionId: 'openRuntimeOptions' },
|
||||||
{ key: 'openJimaku', actionId: 'openJimaku' },
|
{ key: 'openJimaku', actionId: 'openJimaku' },
|
||||||
|
{ key: 'openSessionHelp', actionId: 'openSessionHelp' },
|
||||||
|
{ key: 'openControllerSelect', actionId: 'openControllerSelect' },
|
||||||
|
{ key: 'openControllerDebug', actionId: 'openControllerDebug' },
|
||||||
|
{ key: 'toggleSubtitleSidebar', actionId: 'toggleSubtitleSidebar' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function normalizeModifiers(modifiers: SessionKeyModifier[]): SessionKeyModifier[] {
|
function normalizeModifiers(modifiers: SessionKeyModifier[]): SessionKeyModifier[] {
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
toggleStatsOverlay: false,
|
toggleStatsOverlay: false,
|
||||||
|
toggleSubtitleSidebar: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
|
openSessionHelp: false,
|
||||||
|
openControllerSelect: false,
|
||||||
|
openControllerDebug: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
openPlaylistBrowser: false,
|
openPlaylistBrowser: false,
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export interface ConfiguredShortcuts {
|
|||||||
markAudioCard: string | null | undefined;
|
markAudioCard: string | null | undefined;
|
||||||
openRuntimeOptions: string | null | undefined;
|
openRuntimeOptions: string | null | undefined;
|
||||||
openJimaku: string | null | undefined;
|
openJimaku: string | null | undefined;
|
||||||
|
openSessionHelp: string | null | undefined;
|
||||||
|
openControllerSelect: string | null | undefined;
|
||||||
|
openControllerDebug: string | null | undefined;
|
||||||
|
toggleSubtitleSidebar: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveConfiguredShortcuts(
|
export function resolveConfiguredShortcuts(
|
||||||
@@ -78,5 +82,17 @@ export function resolveConfiguredShortcuts(
|
|||||||
openJimaku: normalizeShortcut(
|
openJimaku: normalizeShortcut(
|
||||||
config.shortcuts?.openJimaku ?? defaultConfig.shortcuts?.openJimaku,
|
config.shortcuts?.openJimaku ?? defaultConfig.shortcuts?.openJimaku,
|
||||||
),
|
),
|
||||||
|
openSessionHelp: normalizeShortcut(
|
||||||
|
config.shortcuts?.openSessionHelp ?? defaultConfig.shortcuts?.openSessionHelp,
|
||||||
|
),
|
||||||
|
openControllerSelect: normalizeShortcut(
|
||||||
|
config.shortcuts?.openControllerSelect ?? defaultConfig.shortcuts?.openControllerSelect,
|
||||||
|
),
|
||||||
|
openControllerDebug: normalizeShortcut(
|
||||||
|
config.shortcuts?.openControllerDebug ?? defaultConfig.shortcuts?.openControllerDebug,
|
||||||
|
),
|
||||||
|
toggleSubtitleSidebar: normalizeShortcut(
|
||||||
|
config.shortcuts?.toggleSubtitleSidebar ?? defaultConfig.shortcuts?.toggleSubtitleSidebar,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
111
src/main.ts
111
src/main.ts
@@ -453,6 +453,10 @@ import { createOverlayModalRuntimeService } from './main/overlay-runtime';
|
|||||||
import { createOverlayModalInputState } from './main/runtime/overlay-modal-input-state';
|
import { createOverlayModalInputState } from './main/runtime/overlay-modal-input-state';
|
||||||
import { openYoutubeTrackPicker } from './main/runtime/youtube-picker-open';
|
import { openYoutubeTrackPicker } from './main/runtime/youtube-picker-open';
|
||||||
import { openRuntimeOptionsModal as openRuntimeOptionsModalRuntime } from './main/runtime/runtime-options-open';
|
import { openRuntimeOptionsModal as openRuntimeOptionsModalRuntime } from './main/runtime/runtime-options-open';
|
||||||
|
import { openJimakuModal as openJimakuModalRuntime } from './main/runtime/jimaku-open';
|
||||||
|
import { openSessionHelpModal as openSessionHelpModalRuntime } from './main/runtime/session-help-open';
|
||||||
|
import { openControllerSelectModal as openControllerSelectModalRuntime } from './main/runtime/controller-select-open';
|
||||||
|
import { openControllerDebugModal as openControllerDebugModalRuntime } from './main/runtime/controller-debug-open';
|
||||||
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
|
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
|
||||||
import { writeSessionBindingsArtifact } from './main/runtime/session-bindings-artifact';
|
import { writeSessionBindingsArtifact } from './main/runtime/session-bindings-artifact';
|
||||||
import { openOverlayHostedModal } from './main/runtime/overlay-hosted-modal-open';
|
import { openOverlayHostedModal } from './main/runtime/overlay-hosted-modal-open';
|
||||||
@@ -2211,8 +2215,21 @@ function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
|||||||
overlayVisibilityComposer.setOverlayDebugVisualizationEnabled(enabled);
|
overlayVisibilityComposer.setOverlayDebugVisualizationEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRuntimeOptionsPalette(): void {
|
function createOverlayHostedModalOpenDeps(): {
|
||||||
void openRuntimeOptionsModalRuntime({
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: {
|
||||||
|
restoreOnModalClose?: OverlayHostedModal;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
) => boolean;
|
||||||
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||||
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
||||||
ensureOverlayWindowsReadyForVisibilityActions(),
|
ensureOverlayWindowsReadyForVisibilityActions(),
|
||||||
@@ -2220,33 +2237,62 @@ function openRuntimeOptionsPalette(): void {
|
|||||||
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||||
waitForModalOpen: (modal, timeoutMs) => overlayModalRuntime.waitForModalOpen(modal, timeoutMs),
|
waitForModalOpen: (modal, timeoutMs) => overlayModalRuntime.waitForModalOpen(modal, timeoutMs),
|
||||||
logWarn: (message) => logger.warn(message),
|
logWarn: (message) => logger.warn(message),
|
||||||
}).then((opened) => {
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openOverlayHostedModalWithOsd(
|
||||||
|
openModal: (deps: ReturnType<typeof createOverlayHostedModalOpenDeps>) => Promise<boolean>,
|
||||||
|
unavailableMessage: string,
|
||||||
|
failureLogMessage: string,
|
||||||
|
): void {
|
||||||
|
void openModal(createOverlayHostedModalOpenDeps()).then((opened) => {
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
showMpvOsd('Runtime options overlay unavailable.');
|
showMpvOsd(unavailableMessage);
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
logger.error('Failed to open runtime options overlay.', error);
|
logger.error(failureLogMessage, error);
|
||||||
showMpvOsd('Runtime options overlay unavailable.');
|
showMpvOsd(unavailableMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openJimakuOverlay(): void {
|
function openRuntimeOptionsPalette(): void {
|
||||||
const opened = openOverlayHostedModal(
|
openOverlayHostedModalWithOsd(
|
||||||
{
|
openRuntimeOptionsModalRuntime,
|
||||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
'Runtime options overlay unavailable.',
|
||||||
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
'Failed to open runtime options overlay.',
|
||||||
ensureOverlayWindowsReadyForVisibilityActions(),
|
|
||||||
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
|
|
||||||
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
channel: IPC_CHANNELS.event.jimakuOpen,
|
|
||||||
modal: 'jimaku',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if (!opened) {
|
|
||||||
showMpvOsd('Jimaku overlay unavailable.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openJimakuOverlay(): void {
|
||||||
|
openOverlayHostedModalWithOsd(
|
||||||
|
openJimakuModalRuntime,
|
||||||
|
'Jimaku overlay unavailable.',
|
||||||
|
'Failed to open Jimaku overlay.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSessionHelpOverlay(): void {
|
||||||
|
openOverlayHostedModalWithOsd(
|
||||||
|
openSessionHelpModalRuntime,
|
||||||
|
'Session help overlay unavailable.',
|
||||||
|
'Failed to open session help overlay.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openControllerSelectOverlay(): void {
|
||||||
|
openOverlayHostedModalWithOsd(
|
||||||
|
openControllerSelectModalRuntime,
|
||||||
|
'Controller select overlay unavailable.',
|
||||||
|
'Failed to open controller select overlay.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openControllerDebugOverlay(): void {
|
||||||
|
openOverlayHostedModalWithOsd(
|
||||||
|
openControllerDebugModalRuntime,
|
||||||
|
'Controller debug overlay unavailable.',
|
||||||
|
'Failed to open controller debug overlay.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPlaylistBrowser(): void {
|
function openPlaylistBrowser(): void {
|
||||||
@@ -2254,16 +2300,11 @@ function openPlaylistBrowser(): void {
|
|||||||
showMpvOsd('Playlist browser requires active playback.');
|
showMpvOsd('Playlist browser requires active playback.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const opened = openPlaylistBrowserRuntime({
|
openOverlayHostedModalWithOsd(
|
||||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
openPlaylistBrowserRuntime,
|
||||||
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
'Playlist browser overlay unavailable.',
|
||||||
ensureOverlayWindowsReadyForVisibilityActions(),
|
'Failed to open playlist browser overlay.',
|
||||||
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
|
);
|
||||||
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
|
||||||
});
|
|
||||||
if (!opened) {
|
|
||||||
showMpvOsd('Playlist browser overlay unavailable.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResolvedConfig() {
|
function getResolvedConfig() {
|
||||||
@@ -4278,6 +4319,10 @@ function handleCycleSecondarySubMode(): void {
|
|||||||
cycleSecondarySubMode();
|
cycleSecondarySubMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSubtitleSidebar(): void {
|
||||||
|
broadcastToOverlayWindows(IPC_CHANNELS.event.subtitleSidebarToggle);
|
||||||
|
}
|
||||||
|
|
||||||
async function triggerSubsyncFromConfig(): Promise<void> {
|
async function triggerSubsyncFromConfig(): Promise<void> {
|
||||||
await subsyncRuntime.triggerFromConfig();
|
await subsyncRuntime.triggerFromConfig();
|
||||||
}
|
}
|
||||||
@@ -4562,9 +4607,13 @@ async function dispatchSessionAction(request: SessionActionDispatchRequest): Pro
|
|||||||
mineSentenceCard: () => mineSentenceCard(),
|
mineSentenceCard: () => mineSentenceCard(),
|
||||||
mineSentenceCount: (count) => handleMineSentenceDigit(count),
|
mineSentenceCount: (count) => handleMineSentenceDigit(count),
|
||||||
toggleSecondarySub: () => handleCycleSecondarySubMode(),
|
toggleSecondarySub: () => handleCycleSecondarySubMode(),
|
||||||
|
toggleSubtitleSidebar: () => toggleSubtitleSidebar(),
|
||||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
openJimaku: () => openJimakuOverlay(),
|
openJimaku: () => openJimakuOverlay(),
|
||||||
|
openSessionHelp: () => openSessionHelpOverlay(),
|
||||||
|
openControllerSelect: () => openControllerSelectOverlay(),
|
||||||
|
openControllerDebug: () => openControllerDebugOverlay(),
|
||||||
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(),
|
||||||
openPlaylistBrowser: () => openPlaylistBrowser(),
|
openPlaylistBrowser: () => openPlaylistBrowser(),
|
||||||
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ test('createMainBootServices builds boot-phase service bundle', () => {
|
|||||||
{ kind: string },
|
{ kind: string },
|
||||||
{ scope: string; warn: () => void; info: () => void; error: () => void },
|
{ scope: string; warn: () => void; info: () => void; error: () => void },
|
||||||
{ registry: boolean },
|
{ registry: boolean },
|
||||||
{ getModalWindow: () => null },
|
{ getMainWindow: () => null; getModalWindow: () => null },
|
||||||
{
|
{
|
||||||
inputState: boolean;
|
inputState: boolean;
|
||||||
getModalInputExclusive: () => boolean;
|
getModalInputExclusive: () => boolean;
|
||||||
@@ -82,6 +82,7 @@ test('createMainBootServices builds boot-phase service bundle', () => {
|
|||||||
}) as const,
|
}) as const,
|
||||||
createMainRuntimeRegistry: () => ({ registry: true }),
|
createMainRuntimeRegistry: () => ({ registry: true }),
|
||||||
createOverlayManager: () => ({
|
createOverlayManager: () => ({
|
||||||
|
getMainWindow: () => null,
|
||||||
getModalWindow: () => null,
|
getModalWindow: () => null,
|
||||||
}),
|
}),
|
||||||
createOverlayModalInputState: () => ({
|
createOverlayModalInputState: () => ({
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export interface MainBootServicesParams<
|
|||||||
getModalWindow: () => BrowserWindow | null;
|
getModalWindow: () => BrowserWindow | null;
|
||||||
syncOverlayShortcutsForModal: (isActive: boolean) => void;
|
syncOverlayShortcutsForModal: (isActive: boolean) => void;
|
||||||
syncOverlayVisibilityForModal: () => void;
|
syncOverlayVisibilityForModal: () => void;
|
||||||
|
restoreMainWindowFocus?: () => void;
|
||||||
}) => TOverlayModalInputState;
|
}) => TOverlayModalInputState;
|
||||||
createOverlayContentMeasurementStore: (params: {
|
createOverlayContentMeasurementStore: (params: {
|
||||||
logger: TLogger;
|
logger: TLogger;
|
||||||
@@ -131,7 +132,7 @@ export function createMainBootServices<
|
|||||||
TSubtitleWebSocket,
|
TSubtitleWebSocket,
|
||||||
TLogger,
|
TLogger,
|
||||||
TRuntimeRegistry,
|
TRuntimeRegistry,
|
||||||
TOverlayManager extends { getModalWindow: () => BrowserWindow | null },
|
TOverlayManager extends { getMainWindow: () => BrowserWindow | null; getModalWindow: () => BrowserWindow | null },
|
||||||
TOverlayModalInputState extends OverlayModalInputStateShape,
|
TOverlayModalInputState extends OverlayModalInputStateShape,
|
||||||
TOverlayContentMeasurementStore,
|
TOverlayContentMeasurementStore,
|
||||||
TOverlayModalRuntime,
|
TOverlayModalRuntime,
|
||||||
@@ -212,6 +213,26 @@ export function createMainBootServices<
|
|||||||
syncOverlayVisibilityForModal: () => {
|
syncOverlayVisibilityForModal: () => {
|
||||||
params.getSyncOverlayVisibilityForModal()();
|
params.getSyncOverlayVisibilityForModal()();
|
||||||
},
|
},
|
||||||
|
restoreMainWindowFocus: () => {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) return;
|
||||||
|
try {
|
||||||
|
const electron = require('electron') as {
|
||||||
|
app?: { focus?: (options?: { steal?: boolean }) => void };
|
||||||
|
};
|
||||||
|
electron.app?.focus?.({ steal: true });
|
||||||
|
} catch {
|
||||||
|
// Ignore in non-Electron environments.
|
||||||
|
}
|
||||||
|
const maybeFocusable = mainWindow as typeof mainWindow & {
|
||||||
|
setFocusable?: (focusable: boolean) => void;
|
||||||
|
};
|
||||||
|
maybeFocusable.setFocusable?.(true);
|
||||||
|
mainWindow.focus();
|
||||||
|
if (!mainWindow.webContents.isFocused()) {
|
||||||
|
mainWindow.webContents.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const overlayContentMeasurementStore = params.createOverlayContentMeasurementStore({
|
const overlayContentMeasurementStore = params.createOverlayContentMeasurementStore({
|
||||||
logger,
|
logger,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ function createMockWindow(): MockWindow & {
|
|||||||
getHideCount: () => number;
|
getHideCount: () => number;
|
||||||
show: () => void;
|
show: () => void;
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
|
destroy: () => void;
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
once: (event: 'ready-to-show', cb: () => void) => void;
|
once: (event: 'ready-to-show', cb: () => void) => void;
|
||||||
webContents: {
|
webContents: {
|
||||||
@@ -81,6 +82,10 @@ function createMockWindow(): MockWindow & {
|
|||||||
state.visible = false;
|
state.visible = false;
|
||||||
state.hideCount += 1;
|
state.hideCount += 1;
|
||||||
},
|
},
|
||||||
|
destroy: () => {
|
||||||
|
state.destroyed = true;
|
||||||
|
state.visible = false;
|
||||||
|
},
|
||||||
focus: () => {
|
focus: () => {
|
||||||
state.focused = true;
|
state.focused = true;
|
||||||
},
|
},
|
||||||
@@ -302,10 +307,10 @@ test('handleOverlayModalClosed hides modal window only after all pending modals
|
|||||||
);
|
);
|
||||||
|
|
||||||
runtime.handleOverlayModalClosed('runtime-options');
|
runtime.handleOverlayModalClosed('runtime-options');
|
||||||
assert.equal(window.getHideCount(), 0);
|
assert.equal(window.isDestroyed(), false);
|
||||||
|
|
||||||
runtime.handleOverlayModalClosed('subsync');
|
runtime.handleOverlayModalClosed('subsync');
|
||||||
assert.equal(window.getHideCount(), 1);
|
assert.equal(window.isDestroyed(), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sendToActiveOverlayWindow prefers visible main overlay window for modal open', () => {
|
test('sendToActiveOverlayWindow prefers visible main overlay window for modal open', () => {
|
||||||
@@ -519,7 +524,7 @@ test('handleOverlayModalClosed is a no-op when no modal window can be targeted',
|
|||||||
assert.deepEqual(state, []);
|
assert.deepEqual(state, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleOverlayModalClosed hides modal window for single kiku modal', () => {
|
test('handleOverlayModalClosed destroys modal window for single kiku modal', () => {
|
||||||
const window = createMockWindow();
|
const window = createMockWindow();
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
@@ -538,11 +543,11 @@ test('handleOverlayModalClosed hides modal window for single kiku modal', () =>
|
|||||||
);
|
);
|
||||||
runtime.handleOverlayModalClosed('kiku');
|
runtime.handleOverlayModalClosed('kiku');
|
||||||
|
|
||||||
assert.equal(window.getHideCount(), 1);
|
assert.equal(window.isDestroyed(), true);
|
||||||
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().size, 0);
|
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().size, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('modal fallback reveal keeps mouse events ignored until modal confirms open', async () => {
|
test('modal fallback reveal skips showing window when content is not ready', async () => {
|
||||||
const window = createMockWindow();
|
const window = createMockWindow();
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
@@ -563,16 +568,15 @@ test('modal fallback reveal keeps mouse events ignored until modal confirms open
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(sent, true);
|
assert.equal(sent, true);
|
||||||
assert.equal(window.ignoreMouseEvents, false);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
setTimeout(resolve, 260);
|
setTimeout(resolve, 260);
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(window.getShowCount(), 1);
|
assert.equal(window.getShowCount(), 0);
|
||||||
assert.equal(window.ignoreMouseEvents, false);
|
|
||||||
|
|
||||||
runtime.notifyOverlayModalOpened('jimaku');
|
runtime.notifyOverlayModalOpened('jimaku');
|
||||||
|
assert.equal(window.getShowCount(), 1);
|
||||||
assert.equal(window.ignoreMouseEvents, false);
|
assert.equal(window.ignoreMouseEvents, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -606,12 +610,19 @@ test('sendToActiveOverlayWindow waits for modal ready-to-show before delivering
|
|||||||
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('warm modal window reopen becomes interactive immediately on the second open', () => {
|
test('modal reopen creates a fresh window after close destroys the previous one', () => {
|
||||||
const window = createMockWindow();
|
const firstWindow = createMockWindow();
|
||||||
|
const secondWindow = createMockWindow();
|
||||||
|
let currentModal: ReturnType<typeof createMockWindow> | null = firstWindow;
|
||||||
|
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
getModalWindow: () => window as never,
|
getModalWindow: () =>
|
||||||
createModalWindow: () => window as never,
|
currentModal && !currentModal.isDestroyed() ? (currentModal as never) : null,
|
||||||
|
createModalWindow: () => {
|
||||||
|
currentModal = secondWindow;
|
||||||
|
return secondWindow as never;
|
||||||
|
},
|
||||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
setModalWindowBounds: () => {},
|
setModalWindowBounds: () => {},
|
||||||
});
|
});
|
||||||
@@ -622,19 +633,84 @@ test('warm modal window reopen becomes interactive immediately on the second ope
|
|||||||
runtime.notifyOverlayModalOpened('runtime-options');
|
runtime.notifyOverlayModalOpened('runtime-options');
|
||||||
runtime.handleOverlayModalClosed('runtime-options');
|
runtime.handleOverlayModalClosed('runtime-options');
|
||||||
|
|
||||||
window.ignoreMouseEvents = true;
|
assert.equal(firstWindow.isDestroyed(), true);
|
||||||
window.focused = false;
|
|
||||||
window.webContentsFocused = false;
|
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(sent, true);
|
||||||
|
assert.equal(currentModal, secondWindow);
|
||||||
|
assert.equal(secondWindow.getShowCount(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('modal reopen after close-destroy notifies state change on fresh window lifecycle', () => {
|
||||||
|
const firstWindow = createMockWindow();
|
||||||
|
const secondWindow = createMockWindow();
|
||||||
|
let currentModal: ReturnType<typeof createMockWindow> | null = firstWindow;
|
||||||
|
const state: boolean[] = [];
|
||||||
|
|
||||||
|
const runtime = createOverlayModalRuntimeService(
|
||||||
|
{
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getModalWindow: () =>
|
||||||
|
currentModal && !currentModal.isDestroyed() ? (currentModal as never) : null,
|
||||||
|
createModalWindow: () => {
|
||||||
|
currentModal = secondWindow;
|
||||||
|
return secondWindow as never;
|
||||||
|
},
|
||||||
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
|
setModalWindowBounds: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onModalStateChange: (active: boolean): void => {
|
||||||
|
state.push(active);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
restoreOnModalClose: 'runtime-options',
|
restoreOnModalClose: 'runtime-options',
|
||||||
});
|
});
|
||||||
|
runtime.notifyOverlayModalOpened('runtime-options');
|
||||||
|
runtime.handleOverlayModalClosed('runtime-options');
|
||||||
|
|
||||||
assert.equal(window.isVisible(), true);
|
assert.deepEqual(state, [true, false]);
|
||||||
|
assert.equal(firstWindow.isDestroyed(), true);
|
||||||
|
|
||||||
|
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
});
|
||||||
|
runtime.notifyOverlayModalOpened('runtime-options');
|
||||||
|
|
||||||
|
assert.deepEqual(state, [true, false, true]);
|
||||||
|
assert.equal(currentModal, secondWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('visible stale modal window is made interactive again before reopening', () => {
|
||||||
|
const window = createMockWindow();
|
||||||
|
window.visible = true;
|
||||||
|
window.focused = true;
|
||||||
|
window.webContentsFocused = false;
|
||||||
|
window.ignoreMouseEvents = true;
|
||||||
|
|
||||||
|
const runtime = createOverlayModalRuntimeService({
|
||||||
|
getMainWindow: () => null,
|
||||||
|
getModalWindow: () => window as never,
|
||||||
|
createModalWindow: () => window as never,
|
||||||
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
|
setModalWindowBounds: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(sent, true);
|
||||||
assert.equal(window.ignoreMouseEvents, false);
|
assert.equal(window.ignoreMouseEvents, false);
|
||||||
assert.equal(window.isFocused(), true);
|
assert.equal(window.isFocused(), true);
|
||||||
assert.equal(window.webContentsFocused, true);
|
assert.equal(window.webContentsFocused, true);
|
||||||
assert.equal(window.getShowCount(), 2);
|
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
|
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
|
||||||
|
|||||||
@@ -5,6 +5,26 @@ import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from '../core/services/overlay-wind
|
|||||||
|
|
||||||
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
|
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
|
||||||
|
|
||||||
|
function requestOverlayApplicationFocus(): void {
|
||||||
|
try {
|
||||||
|
const electron = require('electron') as {
|
||||||
|
app?: {
|
||||||
|
focus?: (options?: { steal?: boolean }) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
electron.app?.focus?.({ steal: true });
|
||||||
|
} catch {
|
||||||
|
// Ignore focus-steal failures in non-Electron test environments.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWindowFocusable(window: BrowserWindow): void {
|
||||||
|
const maybeFocusableWindow = window as BrowserWindow & {
|
||||||
|
setFocusable?: (focusable: boolean) => void;
|
||||||
|
};
|
||||||
|
maybeFocusableWindow.setFocusable?.(true);
|
||||||
|
}
|
||||||
|
|
||||||
export interface OverlayWindowResolver {
|
export interface OverlayWindowResolver {
|
||||||
getMainWindow: () => BrowserWindow | null;
|
getMainWindow: () => BrowserWindow | null;
|
||||||
getModalWindow: () => BrowserWindow | null;
|
getModalWindow: () => BrowserWindow | null;
|
||||||
@@ -142,6 +162,8 @@ export function createOverlayModalRuntimeService(
|
|||||||
passThroughMouseEvents: boolean;
|
passThroughMouseEvents: boolean;
|
||||||
} = { passThroughMouseEvents: false },
|
} = { passThroughMouseEvents: false },
|
||||||
): void => {
|
): void => {
|
||||||
|
setWindowFocusable(window);
|
||||||
|
requestOverlayApplicationFocus();
|
||||||
if (!window.isVisible()) {
|
if (!window.isVisible()) {
|
||||||
window.show();
|
window.show();
|
||||||
}
|
}
|
||||||
@@ -158,15 +180,14 @@ export function createOverlayModalRuntimeService(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ensureModalWindowInteractive = (window: BrowserWindow): void => {
|
const ensureModalWindowInteractive = (window: BrowserWindow): void => {
|
||||||
if (window.isVisible()) {
|
setWindowFocusable(window);
|
||||||
|
requestOverlayApplicationFocus();
|
||||||
window.setIgnoreMouseEvents(false);
|
window.setIgnoreMouseEvents(false);
|
||||||
if (!window.isFocused()) {
|
|
||||||
window.focus();
|
|
||||||
}
|
|
||||||
if (!window.webContents.isFocused()) {
|
|
||||||
window.webContents.focus();
|
|
||||||
}
|
|
||||||
elevateModalWindow(window);
|
elevateModalWindow(window);
|
||||||
|
|
||||||
|
if (window.isVisible()) {
|
||||||
|
window.focus();
|
||||||
|
window.webContents.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +272,9 @@ export function createOverlayModalRuntimeService(
|
|||||||
if (!targetWindow || targetWindow.isDestroyed() || targetWindow.isVisible()) {
|
if (!targetWindow || targetWindow.isDestroyed() || targetWindow.isVisible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isWindowReadyForIpc(targetWindow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
showModalWindow(targetWindow, { passThroughMouseEvents: false });
|
showModalWindow(targetWindow, { passThroughMouseEvents: false });
|
||||||
}, MODAL_REVEAL_FALLBACK_DELAY_MS);
|
}, MODAL_REVEAL_FALLBACK_DELAY_MS);
|
||||||
};
|
};
|
||||||
@@ -306,6 +330,9 @@ export function createOverlayModalRuntimeService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendOrQueueForWindow(modalWindow, (window) => {
|
sendOrQueueForWindow(modalWindow, (window) => {
|
||||||
|
if (window.isVisible()) {
|
||||||
|
ensureModalWindowInteractive(window);
|
||||||
|
}
|
||||||
if (payload === undefined) {
|
if (payload === undefined) {
|
||||||
window.webContents.send(channel);
|
window.webContents.send(channel);
|
||||||
} else {
|
} else {
|
||||||
@@ -346,9 +373,9 @@ export function createOverlayModalRuntimeService(
|
|||||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||||
clearPendingModalWindowReveal();
|
clearPendingModalWindowReveal();
|
||||||
if (modalWindow && !modalWindow.isDestroyed()) {
|
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||||
modalWindow.hide();
|
modalWindow.destroy();
|
||||||
}
|
}
|
||||||
modalWindowPrimedForImmediateShow = true;
|
modalWindowPrimedForImmediateShow = false;
|
||||||
mainWindowMousePassthroughForcedByModal = false;
|
mainWindowMousePassthroughForcedByModal = false;
|
||||||
mainWindowHiddenByModal = false;
|
mainWindowHiddenByModal = false;
|
||||||
notifyModalStateChange(false);
|
notifyModalStateChange(false);
|
||||||
@@ -376,14 +403,7 @@ export function createOverlayModalRuntimeService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (targetWindow.isVisible()) {
|
if (targetWindow.isVisible()) {
|
||||||
targetWindow.setIgnoreMouseEvents(false);
|
ensureModalWindowInteractive(targetWindow);
|
||||||
elevateModalWindow(targetWindow);
|
|
||||||
if (!targetWindow.isFocused()) {
|
|
||||||
targetWindow.focus();
|
|
||||||
}
|
|
||||||
if (!targetWindow.webContents.isFocused()) {
|
|
||||||
targetWindow.webContents.focus();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
src/main/runtime/controller-debug-open.ts
Normal file
48
src/main/runtime/controller-debug-open.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
|
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
|
const CONTROLLER_DEBUG_MODAL: OverlayHostedModal = 'controller-debug';
|
||||||
|
const CONTROLLER_DEBUG_OPEN_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export async function openControllerDebugModal(deps: {
|
||||||
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: {
|
||||||
|
restoreOnModalClose?: OverlayHostedModal;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
) => boolean;
|
||||||
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
return await retryOverlayModalOpen(
|
||||||
|
{
|
||||||
|
waitForModalOpen: deps.waitForModalOpen,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modal: CONTROLLER_DEBUG_MODAL,
|
||||||
|
timeoutMs: CONTROLLER_DEBUG_OPEN_TIMEOUT_MS,
|
||||||
|
retryWarning:
|
||||||
|
'Controller debug modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
sendOpen: () =>
|
||||||
|
openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions:
|
||||||
|
deps.ensureOverlayWindowsReadyForVisibilityActions,
|
||||||
|
sendToActiveOverlayWindow: deps.sendToActiveOverlayWindow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: IPC_CHANNELS.event.controllerDebugOpen,
|
||||||
|
modal: CONTROLLER_DEBUG_MODAL,
|
||||||
|
preferModalWindow: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/main/runtime/controller-select-open.ts
Normal file
48
src/main/runtime/controller-select-open.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
|
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
|
const CONTROLLER_SELECT_MODAL: OverlayHostedModal = 'controller-select';
|
||||||
|
const CONTROLLER_SELECT_OPEN_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export async function openControllerSelectModal(deps: {
|
||||||
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: {
|
||||||
|
restoreOnModalClose?: OverlayHostedModal;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
) => boolean;
|
||||||
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
return await retryOverlayModalOpen(
|
||||||
|
{
|
||||||
|
waitForModalOpen: deps.waitForModalOpen,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modal: CONTROLLER_SELECT_MODAL,
|
||||||
|
timeoutMs: CONTROLLER_SELECT_OPEN_TIMEOUT_MS,
|
||||||
|
retryWarning:
|
||||||
|
'Controller select modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
sendOpen: () =>
|
||||||
|
openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions:
|
||||||
|
deps.ensureOverlayWindowsReadyForVisibilityActions,
|
||||||
|
sendToActiveOverlayWindow: deps.sendToActiveOverlayWindow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: IPC_CHANNELS.event.controllerSelectOpen,
|
||||||
|
modal: CONTROLLER_SELECT_MODAL,
|
||||||
|
preferModalWindow: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -43,7 +43,11 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
triggerSubsync: false,
|
triggerSubsync: false,
|
||||||
markAudioCard: false,
|
markAudioCard: false,
|
||||||
toggleStatsOverlay: false,
|
toggleStatsOverlay: false,
|
||||||
|
toggleSubtitleSidebar: false,
|
||||||
openRuntimeOptions: false,
|
openRuntimeOptions: false,
|
||||||
|
openSessionHelp: false,
|
||||||
|
openControllerSelect: false,
|
||||||
|
openControllerDebug: false,
|
||||||
openJimaku: false,
|
openJimaku: false,
|
||||||
openYoutubePicker: false,
|
openYoutubePicker: false,
|
||||||
openPlaylistBrowser: false,
|
openPlaylistBrowser: false,
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ function createShortcuts(): ConfiguredShortcuts {
|
|||||||
markAudioCard: null,
|
markAudioCard: null,
|
||||||
openRuntimeOptions: null,
|
openRuntimeOptions: null,
|
||||||
openJimaku: null,
|
openJimaku: null,
|
||||||
|
openSessionHelp: null,
|
||||||
|
openControllerSelect: null,
|
||||||
|
openControllerDebug: null,
|
||||||
|
toggleSubtitleSidebar: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ function createShortcuts(): ConfiguredShortcuts {
|
|||||||
markAudioCard: null,
|
markAudioCard: null,
|
||||||
openRuntimeOptions: null,
|
openRuntimeOptions: null,
|
||||||
openJimaku: null,
|
openJimaku: null,
|
||||||
|
openSessionHelp: null,
|
||||||
|
openControllerSelect: null,
|
||||||
|
openControllerDebug: null,
|
||||||
|
toggleSubtitleSidebar: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
src/main/runtime/jimaku-open.ts
Normal file
48
src/main/runtime/jimaku-open.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
|
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
|
const JIMAKU_MODAL: OverlayHostedModal = 'jimaku';
|
||||||
|
const JIMAKU_OPEN_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export async function openJimakuModal(deps: {
|
||||||
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: {
|
||||||
|
restoreOnModalClose?: OverlayHostedModal;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
) => boolean;
|
||||||
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
return await retryOverlayModalOpen(
|
||||||
|
{
|
||||||
|
waitForModalOpen: deps.waitForModalOpen,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modal: JIMAKU_MODAL,
|
||||||
|
timeoutMs: JIMAKU_OPEN_TIMEOUT_MS,
|
||||||
|
retryWarning:
|
||||||
|
'Jimaku modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
sendOpen: () =>
|
||||||
|
openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions:
|
||||||
|
deps.ensureOverlayWindowsReadyForVisibilityActions,
|
||||||
|
sendToActiveOverlayWindow: deps.sendToActiveOverlayWindow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: IPC_CHANNELS.event.jimakuOpen,
|
||||||
|
modal: JIMAKU_MODAL,
|
||||||
|
preferModalWindow: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,3 +27,31 @@ export function openOverlayHostedModal(
|
|||||||
preferModalWindow: input.preferModalWindow,
|
preferModalWindow: input.preferModalWindow,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function retryOverlayModalOpen(
|
||||||
|
deps: {
|
||||||
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
modal: OverlayHostedModal;
|
||||||
|
timeoutMs: number;
|
||||||
|
retryWarning: string;
|
||||||
|
sendOpen: () => boolean;
|
||||||
|
},
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!input.sendOpen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await deps.waitForModalOpen(input.modal, input.timeoutMs)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.logWarn(input.retryWarning);
|
||||||
|
if (!input.sendOpen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await deps.waitForModalOpen(input.modal, input.timeoutMs);
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ function createModalWindow() {
|
|||||||
setIgnoreMouseEvents: (ignore: boolean) => {
|
setIgnoreMouseEvents: (ignore: boolean) => {
|
||||||
calls.push(`ignore:${ignore}`);
|
calls.push(`ignore:${ignore}`);
|
||||||
},
|
},
|
||||||
|
setFocusable: (focusable: boolean) => {
|
||||||
|
calls.push(`focusable:${focusable}`);
|
||||||
|
},
|
||||||
setAlwaysOnTop: (flag: boolean, level?: string, relativeLevel?: number) => {
|
setAlwaysOnTop: (flag: boolean, level?: string, relativeLevel?: number) => {
|
||||||
calls.push(`top:${flag}:${level ?? ''}:${relativeLevel ?? ''}`);
|
calls.push(`top:${flag}:${level ?? ''}:${relativeLevel ?? ''}`);
|
||||||
},
|
},
|
||||||
@@ -58,6 +61,7 @@ test('overlay modal input state activates modal window interactivity and syncs d
|
|||||||
|
|
||||||
assert.equal(state.getModalInputExclusive(), true);
|
assert.equal(state.getModalInputExclusive(), true);
|
||||||
assert.deepEqual(modalWindow.calls, [
|
assert.deepEqual(modalWindow.calls, [
|
||||||
|
'focusable:true',
|
||||||
'ignore:false',
|
'ignore:false',
|
||||||
'top:true:screen-saver:1',
|
'top:true:screen-saver:1',
|
||||||
'focus',
|
'focus',
|
||||||
@@ -66,6 +70,25 @@ test('overlay modal input state activates modal window interactivity and syncs d
|
|||||||
assert.deepEqual(calls, ['shortcuts:true', 'visibility']);
|
assert.deepEqual(calls, ['shortcuts:true', 'visibility']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('overlay modal input state restores main window focus on deactivation', () => {
|
||||||
|
const modalWindow = createModalWindow();
|
||||||
|
const calls: string[] = [];
|
||||||
|
const state = createOverlayModalInputState({
|
||||||
|
getModalWindow: () => modalWindow as never,
|
||||||
|
syncOverlayShortcutsForModal: () => {},
|
||||||
|
syncOverlayVisibilityForModal: () => {},
|
||||||
|
restoreMainWindowFocus: () => {
|
||||||
|
calls.push('restore-focus');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
state.handleModalInputStateChange(true);
|
||||||
|
assert.deepEqual(calls, []);
|
||||||
|
|
||||||
|
state.handleModalInputStateChange(false);
|
||||||
|
assert.deepEqual(calls, ['restore-focus']);
|
||||||
|
});
|
||||||
|
|
||||||
test('overlay modal input state is idempotent for unchanged state', () => {
|
test('overlay modal input state is idempotent for unchanged state', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const state = createOverlayModalInputState({
|
const state = createOverlayModalInputState({
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
function requestOverlayApplicationFocus(): void {
|
||||||
|
try {
|
||||||
|
const electron = require('electron') as {
|
||||||
|
app?: {
|
||||||
|
focus?: (options?: { steal?: boolean }) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
electron.app?.focus?.({ steal: true });
|
||||||
|
} catch {
|
||||||
|
// Ignore focus-steal failures in non-Electron test environments.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWindowFocusable(window: BrowserWindow): void {
|
||||||
|
const maybeFocusableWindow = window as BrowserWindow & {
|
||||||
|
setFocusable?: (focusable: boolean) => void;
|
||||||
|
};
|
||||||
|
maybeFocusableWindow.setFocusable?.(true);
|
||||||
|
}
|
||||||
|
|
||||||
export type OverlayModalInputStateDeps = {
|
export type OverlayModalInputStateDeps = {
|
||||||
getModalWindow: () => BrowserWindow | null;
|
getModalWindow: () => BrowserWindow | null;
|
||||||
syncOverlayShortcutsForModal: (isActive: boolean) => void;
|
syncOverlayShortcutsForModal: (isActive: boolean) => void;
|
||||||
syncOverlayVisibilityForModal: () => void;
|
syncOverlayVisibilityForModal: () => void;
|
||||||
|
restoreMainWindowFocus?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createOverlayModalInputState(deps: OverlayModalInputStateDeps) {
|
export function createOverlayModalInputState(deps: OverlayModalInputStateDeps) {
|
||||||
@@ -18,6 +39,8 @@ export function createOverlayModalInputState(deps: OverlayModalInputStateDeps) {
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
const modalWindow = deps.getModalWindow();
|
const modalWindow = deps.getModalWindow();
|
||||||
if (modalWindow && !modalWindow.isDestroyed()) {
|
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||||
|
setWindowFocusable(modalWindow);
|
||||||
|
requestOverlayApplicationFocus();
|
||||||
modalWindow.setIgnoreMouseEvents(false);
|
modalWindow.setIgnoreMouseEvents(false);
|
||||||
modalWindow.setAlwaysOnTop(true, 'screen-saver', 1);
|
modalWindow.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||||
modalWindow.focus();
|
modalWindow.focus();
|
||||||
@@ -29,6 +52,9 @@ export function createOverlayModalInputState(deps: OverlayModalInputStateDeps) {
|
|||||||
|
|
||||||
deps.syncOverlayShortcutsForModal(isActive);
|
deps.syncOverlayShortcutsForModal(isActive);
|
||||||
deps.syncOverlayVisibilityForModal();
|
deps.syncOverlayVisibilityForModal();
|
||||||
|
if (!isActive) {
|
||||||
|
deps.restoreMainWindowFocus?.();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import test from 'node:test';
|
|||||||
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
import { openPlaylistBrowser } from './playlist-browser-open';
|
import { openPlaylistBrowser } from './playlist-browser-open';
|
||||||
|
|
||||||
test('playlist browser open bootstraps overlay runtime before dispatching the modal event', () => {
|
test('playlist browser open bootstraps overlay runtime and sends modal event with preferModalWindow', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|
||||||
const opened = openPlaylistBrowser({
|
const opened = await openPlaylistBrowser({
|
||||||
ensureOverlayStartupPrereqs: () => {
|
ensureOverlayStartupPrereqs: () => {
|
||||||
calls.push('prereqs');
|
calls.push('prereqs');
|
||||||
},
|
},
|
||||||
@@ -18,11 +18,31 @@ test('playlist browser open bootstraps overlay runtime before dispatching the mo
|
|||||||
assert.equal(payload, undefined);
|
assert.equal(payload, undefined);
|
||||||
assert.deepEqual(runtimeOptions, {
|
assert.deepEqual(runtimeOptions, {
|
||||||
restoreOnModalClose: 'playlist-browser',
|
restoreOnModalClose: 'playlist-browser',
|
||||||
|
preferModalWindow: true,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
waitForModalOpen: async () => true,
|
||||||
|
logWarn: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(opened, true);
|
assert.equal(opened, true);
|
||||||
assert.deepEqual(calls, ['prereqs', 'windows', `send:${IPC_CHANNELS.event.playlistBrowserOpen}`]);
|
assert.deepEqual(calls, ['prereqs', 'windows', `send:${IPC_CHANNELS.event.playlistBrowserOpen}`]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('playlist browser open retries after first attempt timeout', async () => {
|
||||||
|
let attempt = 0;
|
||||||
|
const opened = await openPlaylistBrowser({
|
||||||
|
ensureOverlayStartupPrereqs: () => {},
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => {},
|
||||||
|
sendToActiveOverlayWindow: () => true,
|
||||||
|
waitForModalOpen: async () => {
|
||||||
|
attempt += 1;
|
||||||
|
return attempt >= 2;
|
||||||
|
},
|
||||||
|
logWarn: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(opened, true);
|
||||||
|
assert.equal(attempt, 2);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
|
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
const PLAYLIST_BROWSER_MODAL: OverlayHostedModal = 'playlist-browser';
|
const PLAYLIST_BROWSER_MODAL: OverlayHostedModal = 'playlist-browser';
|
||||||
|
const PLAYLIST_BROWSER_OPEN_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
export function openPlaylistBrowser(deps: {
|
export async function openPlaylistBrowser(deps: {
|
||||||
ensureOverlayStartupPrereqs: () => void;
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
sendToActiveOverlayWindow: (
|
sendToActiveOverlayWindow: (
|
||||||
@@ -14,10 +16,33 @@ export function openPlaylistBrowser(deps: {
|
|||||||
preferModalWindow?: boolean;
|
preferModalWindow?: boolean;
|
||||||
},
|
},
|
||||||
) => boolean;
|
) => boolean;
|
||||||
}): boolean {
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
deps.ensureOverlayStartupPrereqs();
|
logWarn: (message: string) => void;
|
||||||
deps.ensureOverlayWindowsReadyForVisibilityActions();
|
}): Promise<boolean> {
|
||||||
return deps.sendToActiveOverlayWindow(IPC_CHANNELS.event.playlistBrowserOpen, undefined, {
|
return await retryOverlayModalOpen(
|
||||||
restoreOnModalClose: PLAYLIST_BROWSER_MODAL,
|
{
|
||||||
});
|
waitForModalOpen: deps.waitForModalOpen,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modal: PLAYLIST_BROWSER_MODAL,
|
||||||
|
timeoutMs: PLAYLIST_BROWSER_OPEN_TIMEOUT_MS,
|
||||||
|
retryWarning:
|
||||||
|
'Playlist browser modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
sendOpen: () =>
|
||||||
|
openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions:
|
||||||
|
deps.ensureOverlayWindowsReadyForVisibilityActions,
|
||||||
|
sendToActiveOverlayWindow: deps.sendToActiveOverlayWindow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: IPC_CHANNELS.event.playlistBrowserOpen,
|
||||||
|
modal: PLAYLIST_BROWSER_MODAL,
|
||||||
|
preferModalWindow: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
import { openOverlayHostedModal } from './overlay-hosted-modal-open';
|
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
const RUNTIME_OPTIONS_MODAL: OverlayHostedModal = 'runtime-options';
|
const RUNTIME_OPTIONS_MODAL: OverlayHostedModal = 'runtime-options';
|
||||||
const RUNTIME_OPTIONS_OPEN_TIMEOUT_MS = 1500;
|
const RUNTIME_OPTIONS_OPEN_TIMEOUT_MS = 1500;
|
||||||
@@ -18,8 +18,18 @@ export async function openRuntimeOptionsModal(deps: {
|
|||||||
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
logWarn: (message: string) => void;
|
logWarn: (message: string) => void;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const sendOpen = (): boolean => {
|
return await retryOverlayModalOpen(
|
||||||
return openOverlayHostedModal(
|
{
|
||||||
|
waitForModalOpen: deps.waitForModalOpen,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modal: RUNTIME_OPTIONS_MODAL,
|
||||||
|
timeoutMs: RUNTIME_OPTIONS_OPEN_TIMEOUT_MS,
|
||||||
|
retryWarning:
|
||||||
|
'Runtime options modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
sendOpen: () =>
|
||||||
|
openOverlayHostedModal(
|
||||||
{
|
{
|
||||||
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
|
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
|
||||||
ensureOverlayWindowsReadyForVisibilityActions:
|
ensureOverlayWindowsReadyForVisibilityActions:
|
||||||
@@ -31,23 +41,7 @@ export async function openRuntimeOptionsModal(deps: {
|
|||||||
modal: RUNTIME_OPTIONS_MODAL,
|
modal: RUNTIME_OPTIONS_MODAL,
|
||||||
preferModalWindow: true,
|
preferModalWindow: true,
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
if (!sendOpen()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await deps.waitForModalOpen(RUNTIME_OPTIONS_MODAL, RUNTIME_OPTIONS_OPEN_TIMEOUT_MS)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
deps.logWarn(
|
|
||||||
'Runtime options modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
|
||||||
);
|
|
||||||
if (!sendOpen()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await deps.waitForModalOpen(RUNTIME_OPTIONS_MODAL, RUNTIME_OPTIONS_OPEN_TIMEOUT_MS);
|
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/main/runtime/session-help-open.ts
Normal file
48
src/main/runtime/session-help-open.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
|
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
|
const SESSION_HELP_MODAL: OverlayHostedModal = 'session-help';
|
||||||
|
const SESSION_HELP_OPEN_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export async function openSessionHelpModal(deps: {
|
||||||
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: {
|
||||||
|
restoreOnModalClose?: OverlayHostedModal;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
) => boolean;
|
||||||
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
return await retryOverlayModalOpen(
|
||||||
|
{
|
||||||
|
waitForModalOpen: deps.waitForModalOpen,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modal: SESSION_HELP_MODAL,
|
||||||
|
timeoutMs: SESSION_HELP_OPEN_TIMEOUT_MS,
|
||||||
|
retryWarning:
|
||||||
|
'Session help modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
sendOpen: () =>
|
||||||
|
openOverlayHostedModal(
|
||||||
|
{
|
||||||
|
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions:
|
||||||
|
deps.ensureOverlayWindowsReadyForVisibilityActions,
|
||||||
|
sendToActiveOverlayWindow: deps.sendToActiveOverlayWindow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: IPC_CHANNELS.event.sessionHelpOpen,
|
||||||
|
modal: SESSION_HELP_MODAL,
|
||||||
|
preferModalWindow: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { YoutubePickerOpenPayload } from '../../types';
|
import type { YoutubePickerOpenPayload } from '../../types';
|
||||||
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
|
import { retryOverlayModalOpen } from './overlay-hosted-modal-open';
|
||||||
|
|
||||||
const YOUTUBE_PICKER_MODAL: OverlayHostedModal = 'youtube-track-picker';
|
const YOUTUBE_PICKER_MODAL: OverlayHostedModal = 'youtube-track-picker';
|
||||||
const YOUTUBE_PICKER_OPEN_TIMEOUT_MS = 1500;
|
const YOUTUBE_PICKER_OPEN_TIMEOUT_MS = 1500;
|
||||||
@@ -19,24 +20,21 @@ export async function openYoutubeTrackPicker(
|
|||||||
},
|
},
|
||||||
payload: YoutubePickerOpenPayload,
|
payload: YoutubePickerOpenPayload,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const sendPickerOpen = (): boolean =>
|
return await retryOverlayModalOpen(
|
||||||
|
{
|
||||||
|
waitForModalOpen: deps.waitForModalOpen,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modal: YOUTUBE_PICKER_MODAL,
|
||||||
|
timeoutMs: YOUTUBE_PICKER_OPEN_TIMEOUT_MS,
|
||||||
|
retryWarning:
|
||||||
|
'YouTube subtitle picker did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
sendOpen: () =>
|
||||||
deps.sendToActiveOverlayWindow('youtube:picker-open', payload, {
|
deps.sendToActiveOverlayWindow('youtube:picker-open', payload, {
|
||||||
restoreOnModalClose: YOUTUBE_PICKER_MODAL,
|
restoreOnModalClose: YOUTUBE_PICKER_MODAL,
|
||||||
preferModalWindow: true,
|
preferModalWindow: true,
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
if (!sendPickerOpen()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (await deps.waitForModalOpen(YOUTUBE_PICKER_MODAL, YOUTUBE_PICKER_OPEN_TIMEOUT_MS)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
deps.logWarn(
|
|
||||||
'YouTube subtitle picker did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
|
||||||
);
|
);
|
||||||
if (!sendPickerOpen()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return await deps.waitForModalOpen(YOUTUBE_PICKER_MODAL, YOUTUBE_PICKER_OPEN_TIMEOUT_MS);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ function createQueuedIpcListenerWithPayload<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen);
|
const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen);
|
||||||
|
const onOpenSessionHelpEvent = createQueuedIpcListener(IPC_CHANNELS.event.sessionHelpOpen);
|
||||||
|
const onOpenControllerSelectEvent = createQueuedIpcListener(IPC_CHANNELS.event.controllerSelectOpen);
|
||||||
|
const onOpenControllerDebugEvent = createQueuedIpcListener(IPC_CHANNELS.event.controllerDebugOpen);
|
||||||
const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen);
|
const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen);
|
||||||
const onOpenYoutubeTrackPickerEvent = createQueuedIpcListenerWithPayload<YoutubePickerOpenPayload>(
|
const onOpenYoutubeTrackPickerEvent = createQueuedIpcListenerWithPayload<YoutubePickerOpenPayload>(
|
||||||
IPC_CHANNELS.event.youtubePickerOpen,
|
IPC_CHANNELS.event.youtubePickerOpen,
|
||||||
@@ -142,6 +145,9 @@ const onSubsyncManualOpenEvent = createQueuedIpcListenerWithPayload<SubsyncManua
|
|||||||
IPC_CHANNELS.event.subsyncOpenManual,
|
IPC_CHANNELS.event.subsyncOpenManual,
|
||||||
(payload) => payload as SubsyncManualPayload,
|
(payload) => payload as SubsyncManualPayload,
|
||||||
);
|
);
|
||||||
|
const onSubtitleSidebarToggleEvent = createQueuedIpcListener(
|
||||||
|
IPC_CHANNELS.event.subtitleSidebarToggle,
|
||||||
|
);
|
||||||
const onKikuFieldGroupingRequestEvent =
|
const onKikuFieldGroupingRequestEvent =
|
||||||
createQueuedIpcListenerWithPayload<KikuFieldGroupingRequestData>(
|
createQueuedIpcListenerWithPayload<KikuFieldGroupingRequestData>(
|
||||||
IPC_CHANNELS.event.kikuFieldGroupingRequest,
|
IPC_CHANNELS.event.kikuFieldGroupingRequest,
|
||||||
@@ -326,9 +332,13 @@ const electronAPI: ElectronAPI = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onOpenRuntimeOptions: onOpenRuntimeOptionsEvent,
|
onOpenRuntimeOptions: onOpenRuntimeOptionsEvent,
|
||||||
|
onOpenSessionHelp: onOpenSessionHelpEvent,
|
||||||
|
onOpenControllerSelect: onOpenControllerSelectEvent,
|
||||||
|
onOpenControllerDebug: onOpenControllerDebugEvent,
|
||||||
onOpenJimaku: onOpenJimakuEvent,
|
onOpenJimaku: onOpenJimakuEvent,
|
||||||
onOpenYoutubeTrackPicker: onOpenYoutubeTrackPickerEvent,
|
onOpenYoutubeTrackPicker: onOpenYoutubeTrackPickerEvent,
|
||||||
onOpenPlaylistBrowser: onOpenPlaylistBrowserEvent,
|
onOpenPlaylistBrowser: onOpenPlaylistBrowserEvent,
|
||||||
|
onSubtitleSidebarToggle: onSubtitleSidebarToggleEvent,
|
||||||
onCancelYoutubeTrackPicker: onCancelYoutubeTrackPickerEvent,
|
onCancelYoutubeTrackPicker: onCancelYoutubeTrackPickerEvent,
|
||||||
onKeyboardModeToggleRequested: onKeyboardModeToggleRequestedEvent,
|
onKeyboardModeToggleRequested: onKeyboardModeToggleRequestedEvent,
|
||||||
onLookupWindowToggleRequested: onLookupWindowToggleRequestedEvent,
|
onLookupWindowToggleRequested: onLookupWindowToggleRequestedEvent,
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ function installKeyboardTestGlobals() {
|
|||||||
markAudioCard: '',
|
markAudioCard: '',
|
||||||
openRuntimeOptions: 'CommandOrControl+Shift+O',
|
openRuntimeOptions: 'CommandOrControl+Shift+O',
|
||||||
openJimaku: 'Ctrl+Shift+J',
|
openJimaku: 'Ctrl+Shift+J',
|
||||||
|
openSessionHelp: 'CommandOrControl+Shift+H',
|
||||||
|
openControllerSelect: 'Alt+C',
|
||||||
|
openControllerDebug: 'Alt+Shift+C',
|
||||||
|
toggleSubtitleSidebar: '',
|
||||||
toggleVisibleOverlayGlobal: '',
|
toggleVisibleOverlayGlobal: '',
|
||||||
};
|
};
|
||||||
let markActiveVideoWatchedResult = true;
|
let markActiveVideoWatchedResult = true;
|
||||||
@@ -321,8 +325,6 @@ function installKeyboardTestGlobals() {
|
|||||||
function createKeyboardHandlerHarness() {
|
function createKeyboardHandlerHarness() {
|
||||||
const testGlobals = installKeyboardTestGlobals();
|
const testGlobals = installKeyboardTestGlobals();
|
||||||
const subtitleRootClassList = createClassList();
|
const subtitleRootClassList = createClassList();
|
||||||
let controllerSelectOpenCount = 0;
|
|
||||||
let controllerDebugOpenCount = 0;
|
|
||||||
let controllerSelectKeydownCount = 0;
|
let controllerSelectKeydownCount = 0;
|
||||||
let playlistBrowserKeydownCount = 0;
|
let playlistBrowserKeydownCount = 0;
|
||||||
|
|
||||||
@@ -373,20 +375,12 @@ function createKeyboardHandlerHarness() {
|
|||||||
openSessionHelpModal: () => {},
|
openSessionHelpModal: () => {},
|
||||||
appendClipboardVideoToQueue: () => {},
|
appendClipboardVideoToQueue: () => {},
|
||||||
getPlaybackPaused: () => testGlobals.getPlaybackPaused(),
|
getPlaybackPaused: () => testGlobals.getPlaybackPaused(),
|
||||||
openControllerSelectModal: () => {
|
|
||||||
controllerSelectOpenCount += 1;
|
|
||||||
},
|
|
||||||
openControllerDebugModal: () => {
|
|
||||||
controllerDebugOpenCount += 1;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ctx,
|
ctx,
|
||||||
handlers,
|
handlers,
|
||||||
testGlobals,
|
testGlobals,
|
||||||
controllerSelectOpenCount: () => controllerSelectOpenCount,
|
|
||||||
controllerDebugOpenCount: () => controllerDebugOpenCount,
|
|
||||||
controllerSelectKeydownCount: () => controllerSelectKeydownCount,
|
controllerSelectKeydownCount: () => controllerSelectKeydownCount,
|
||||||
playlistBrowserKeydownCount: () => playlistBrowserKeydownCount,
|
playlistBrowserKeydownCount: () => playlistBrowserKeydownCount,
|
||||||
setWordCount: (count: number) => {
|
setWordCount: (count: number) => {
|
||||||
@@ -659,31 +653,78 @@ test('keyboard mode: controller helpers dispatch popup audio play/cycle and scro
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keyboard mode: Alt+Shift+C opens controller debug modal', async () => {
|
test('keyboard mode: configured controller debug binding dispatches session action', async () => {
|
||||||
const { testGlobals, handlers, controllerDebugOpenCount } = createKeyboardHandlerHarness();
|
const { testGlobals, handlers } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handlers.setupMpvInputForwarding();
|
await handlers.setupMpvInputForwarding();
|
||||||
|
handlers.updateSessionBindings([
|
||||||
|
{
|
||||||
|
sourcePath: 'shortcuts.openControllerDebug',
|
||||||
|
originalKey: 'Alt+Shift+D',
|
||||||
|
key: { code: 'KeyD', modifiers: ['alt', 'shift'] },
|
||||||
|
actionType: 'session-action',
|
||||||
|
actionId: 'openControllerDebug',
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
|
||||||
testGlobals.dispatchKeydown({
|
testGlobals.dispatchKeydown({
|
||||||
key: 'C',
|
key: 'D',
|
||||||
code: 'KeyC',
|
code: 'KeyD',
|
||||||
altKey: true,
|
altKey: true,
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(controllerDebugOpenCount(), 1);
|
assert.deepEqual(testGlobals.sessionActions, [{ actionId: 'openControllerDebug', payload: undefined }]);
|
||||||
} finally {
|
} finally {
|
||||||
testGlobals.restore();
|
testGlobals.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keyboard mode: Alt+Shift+C opens controller debug modal even while popup is visible', async () => {
|
test('keyboard mode: configured controller debug binding is not swallowed while popup is visible', async () => {
|
||||||
const { ctx, testGlobals, handlers, controllerDebugOpenCount } = createKeyboardHandlerHarness();
|
const { ctx, testGlobals, handlers } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handlers.setupMpvInputForwarding();
|
await handlers.setupMpvInputForwarding();
|
||||||
ctx.state.yomitanPopupVisible = true;
|
ctx.state.yomitanPopupVisible = true;
|
||||||
|
testGlobals.setPopupVisible(true);
|
||||||
|
handlers.updateSessionBindings([
|
||||||
|
{
|
||||||
|
sourcePath: 'shortcuts.openControllerDebug',
|
||||||
|
originalKey: 'Alt+Shift+D',
|
||||||
|
key: { code: 'KeyD', modifiers: ['alt', 'shift'] },
|
||||||
|
actionType: 'session-action',
|
||||||
|
actionId: 'openControllerDebug',
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
|
||||||
|
testGlobals.dispatchKeydown({
|
||||||
|
key: 'D',
|
||||||
|
code: 'KeyD',
|
||||||
|
altKey: true,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(testGlobals.sessionActions, [{ actionId: 'openControllerDebug', payload: undefined }]);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keyboard mode: former fixed Alt+Shift+C does nothing when controller debug is remapped', async () => {
|
||||||
|
const { testGlobals, handlers } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
handlers.updateSessionBindings([
|
||||||
|
{
|
||||||
|
sourcePath: 'shortcuts.openControllerDebug',
|
||||||
|
originalKey: 'Alt+Shift+D',
|
||||||
|
key: { code: 'KeyD', modifiers: ['alt', 'shift'] },
|
||||||
|
actionType: 'session-action',
|
||||||
|
actionId: 'openControllerDebug',
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
|
||||||
testGlobals.dispatchKeydown({
|
testGlobals.dispatchKeydown({
|
||||||
key: 'C',
|
key: 'C',
|
||||||
@@ -692,7 +733,7 @@ test('keyboard mode: Alt+Shift+C opens controller debug modal even while popup i
|
|||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(controllerDebugOpenCount(), 1);
|
assert.deepEqual(testGlobals.sessionActions, []);
|
||||||
} finally {
|
} finally {
|
||||||
testGlobals.restore();
|
testGlobals.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export function createKeyboardHandlers(
|
|||||||
}) => void;
|
}) => void;
|
||||||
appendClipboardVideoToQueue: () => void;
|
appendClipboardVideoToQueue: () => void;
|
||||||
getPlaybackPaused: () => Promise<boolean | null>;
|
getPlaybackPaused: () => Promise<boolean | null>;
|
||||||
openControllerSelectModal: () => void;
|
|
||||||
openControllerDebugModal: () => void;
|
|
||||||
toggleSubtitleSidebarModal?: () => void;
|
toggleSubtitleSidebarModal?: () => void;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -298,10 +296,6 @@ export function createKeyboardHandlers(
|
|||||||
return isPrimaryModifierPressed(e) && !e.altKey && !e.shiftKey && isYKey && !e.repeat;
|
return isPrimaryModifierPressed(e) && !e.altKey && !e.shiftKey && isYKey && !e.repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isControllerModalShortcut(e: KeyboardEvent): boolean {
|
|
||||||
return !e.ctrlKey && !e.metaKey && e.altKey && !e.repeat && e.code === 'KeyC';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSubtitleSidebarToggle(e: KeyboardEvent): boolean {
|
function isSubtitleSidebarToggle(e: KeyboardEvent): boolean {
|
||||||
const toggleKey = ctx.state.subtitleSidebarToggleKey;
|
const toggleKey = ctx.state.subtitleSidebarToggleKey;
|
||||||
if (!toggleKey) return false;
|
if (!toggleKey) return false;
|
||||||
@@ -1040,10 +1034,7 @@ export function createKeyboardHandlers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
|
||||||
(ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) &&
|
|
||||||
!isControllerModalShortcut(e)
|
|
||||||
) {
|
|
||||||
if (handleYomitanPopupKeybind(e)) {
|
if (handleYomitanPopupKeybind(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -1100,16 +1091,6 @@ export function createKeyboardHandlers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isControllerModalShortcut(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.shiftKey) {
|
|
||||||
options.openControllerDebugModal();
|
|
||||||
} else {
|
|
||||||
options.openControllerSelectModal();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyString = keyEventToString(e);
|
const keyString = keyEventToString(e);
|
||||||
const binding = ctx.state.sessionBindingMap.get(keyString);
|
const binding = ctx.state.sessionBindingMap.get(keyString);
|
||||||
if (binding) {
|
if (binding) {
|
||||||
|
|||||||
@@ -586,31 +586,10 @@ export function createSessionHelpModal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSessionHelpModal(opening: SessionHelpBindingInfo): Promise<void> {
|
function openSessionHelpModal(opening: SessionHelpBindingInfo): void {
|
||||||
openBinding = opening;
|
openBinding = opening;
|
||||||
priorFocus = document.activeElement;
|
priorFocus = document.activeElement;
|
||||||
|
|
||||||
const dataLoaded = await render();
|
|
||||||
|
|
||||||
ctx.dom.sessionHelpShortcut.textContent = `Session help opened with ${formatBindingHint(openBinding)}`;
|
|
||||||
if (openBinding.fallbackUnavailable) {
|
|
||||||
ctx.dom.sessionHelpWarning.textContent =
|
|
||||||
'Both Y-H and Y-K are bound; Y-K remains the fallback for this session.';
|
|
||||||
} else if (openBinding.fallbackUsed) {
|
|
||||||
ctx.dom.sessionHelpWarning.textContent = 'Y-H is already bound; using Y-K as fallback.';
|
|
||||||
} else {
|
|
||||||
ctx.dom.sessionHelpWarning.textContent = '';
|
|
||||||
}
|
|
||||||
if (dataLoaded) {
|
|
||||||
ctx.dom.sessionHelpStatus.textContent =
|
|
||||||
'Use Arrow keys, J/K/H/L, mouse, click, or / then type to filter. Esc closes.';
|
|
||||||
} else {
|
|
||||||
ctx.dom.sessionHelpStatus.textContent =
|
|
||||||
'Session help data is unavailable right now. Press Esc to close.';
|
|
||||||
ctx.dom.sessionHelpWarning.textContent =
|
|
||||||
'Unable to load latest shortcut settings from the runtime.';
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.state.sessionHelpModalOpen = true;
|
ctx.state.sessionHelpModalOpen = true;
|
||||||
options.syncSettingsModalSubtitleSuppression();
|
options.syncSettingsModalSubtitleSuppression();
|
||||||
ctx.dom.overlay.classList.add('interactive');
|
ctx.dom.overlay.classList.add('interactive');
|
||||||
@@ -623,6 +602,17 @@ export function createSessionHelpModal(
|
|||||||
window.electronAPI.setIgnoreMouseEvents(false);
|
window.electronAPI.setIgnoreMouseEvents(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.dom.sessionHelpShortcut.textContent = `Session help opened with ${formatBindingHint(openBinding)}`;
|
||||||
|
if (openBinding.fallbackUnavailable) {
|
||||||
|
ctx.dom.sessionHelpWarning.textContent =
|
||||||
|
'Both Y-H and Y-K are bound; Y-K remains the fallback for this session.';
|
||||||
|
} else if (openBinding.fallbackUsed) {
|
||||||
|
ctx.dom.sessionHelpWarning.textContent = 'Y-H is already bound; using Y-K as fallback.';
|
||||||
|
} else {
|
||||||
|
ctx.dom.sessionHelpWarning.textContent = '';
|
||||||
|
}
|
||||||
|
ctx.dom.sessionHelpStatus.textContent = 'Loading session help data...';
|
||||||
|
|
||||||
if (focusGuard === null) {
|
if (focusGuard === null) {
|
||||||
focusGuard = (event: FocusEvent) => {
|
focusGuard = (event: FocusEvent) => {
|
||||||
if (!ctx.state.sessionHelpModalOpen) return;
|
if (!ctx.state.sessionHelpModalOpen) return;
|
||||||
@@ -639,6 +629,19 @@ export function createSessionHelpModal(
|
|||||||
requestOverlayFocus();
|
requestOverlayFocus();
|
||||||
window.focus();
|
window.focus();
|
||||||
enforceModalFocus();
|
enforceModalFocus();
|
||||||
|
|
||||||
|
void render().then((dataLoaded) => {
|
||||||
|
if (!ctx.state.sessionHelpModalOpen) return;
|
||||||
|
if (dataLoaded) {
|
||||||
|
ctx.dom.sessionHelpStatus.textContent =
|
||||||
|
'Use Arrow keys, J/K/H/L, mouse, click, or / then type to filter. Esc closes.';
|
||||||
|
} else {
|
||||||
|
ctx.dom.sessionHelpStatus.textContent =
|
||||||
|
'Session help data is unavailable right now. Press Esc to close.';
|
||||||
|
ctx.dom.sessionHelpWarning.textContent =
|
||||||
|
'Unable to load latest shortcut settings from the runtime.';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSessionHelpModal(): void {
|
function closeSessionHelpModal(): void {
|
||||||
@@ -648,6 +651,7 @@ export function createSessionHelpModal(
|
|||||||
options.syncSettingsModalSubtitleSuppression();
|
options.syncSettingsModalSubtitleSuppression();
|
||||||
ctx.dom.sessionHelpModal.classList.add('hidden');
|
ctx.dom.sessionHelpModal.classList.add('hidden');
|
||||||
ctx.dom.sessionHelpModal.setAttribute('aria-hidden', 'true');
|
ctx.dom.sessionHelpModal.setAttribute('aria-hidden', 'true');
|
||||||
|
window.electronAPI.notifyOverlayModalClosed('session-help');
|
||||||
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) {
|
||||||
ctx.dom.overlay.classList.remove('interactive');
|
ctx.dom.overlay.classList.remove('interactive');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,14 +178,6 @@ const keyboardHandlers = createKeyboardHandlers(ctx, {
|
|||||||
void window.electronAPI.appendClipboardVideoToQueue();
|
void window.electronAPI.appendClipboardVideoToQueue();
|
||||||
},
|
},
|
||||||
getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
|
getPlaybackPaused: () => window.electronAPI.getPlaybackPaused(),
|
||||||
openControllerSelectModal: () => {
|
|
||||||
controllerSelectModal.openControllerSelectModal();
|
|
||||||
window.electronAPI.notifyOverlayModalOpened('controller-select');
|
|
||||||
},
|
|
||||||
openControllerDebugModal: () => {
|
|
||||||
controllerDebugModal.openControllerDebugModal();
|
|
||||||
window.electronAPI.notifyOverlayModalOpened('controller-debug');
|
|
||||||
},
|
|
||||||
toggleSubtitleSidebarModal: () => {
|
toggleSubtitleSidebarModal: () => {
|
||||||
void subtitleSidebarModal.toggleSubtitleSidebarModal();
|
void subtitleSidebarModal.toggleSubtitleSidebarModal();
|
||||||
},
|
},
|
||||||
@@ -437,6 +429,28 @@ function registerModalOpenHandlers(): void {
|
|||||||
window.electronAPI.notifyOverlayModalOpened('runtime-options');
|
window.electronAPI.notifyOverlayModalOpened('runtime-options');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
window.electronAPI.onOpenSessionHelp(() => {
|
||||||
|
runGuarded('session-help:open', () => {
|
||||||
|
sessionHelpModal.openSessionHelpModal({
|
||||||
|
bindingKey: 'KeyH',
|
||||||
|
fallbackUsed: false,
|
||||||
|
fallbackUnavailable: false,
|
||||||
|
});
|
||||||
|
window.electronAPI.notifyOverlayModalOpened('session-help');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.electronAPI.onOpenControllerSelect(() => {
|
||||||
|
runGuarded('controller-select:open', () => {
|
||||||
|
controllerSelectModal.openControllerSelectModal();
|
||||||
|
window.electronAPI.notifyOverlayModalOpened('controller-select');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.electronAPI.onOpenControllerDebug(() => {
|
||||||
|
runGuarded('controller-debug:open', () => {
|
||||||
|
controllerDebugModal.openControllerDebugModal();
|
||||||
|
window.electronAPI.notifyOverlayModalOpened('controller-debug');
|
||||||
|
});
|
||||||
|
});
|
||||||
window.electronAPI.onOpenJimaku(() => {
|
window.electronAPI.onOpenJimaku(() => {
|
||||||
runGuarded('jimaku:open', () => {
|
runGuarded('jimaku:open', () => {
|
||||||
jimakuModal.openJimakuModal();
|
jimakuModal.openJimakuModal();
|
||||||
@@ -492,6 +506,12 @@ function registerKeyboardCommandHandlers(): void {
|
|||||||
keyboardHandlers.handleLookupWindowToggleRequested();
|
keyboardHandlers.handleLookupWindowToggleRequested();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.electronAPI.onSubtitleSidebarToggle(() => {
|
||||||
|
runGuarded('subtitle-sidebar:toggle', () => {
|
||||||
|
void subtitleSidebarModal.toggleSubtitleSidebarModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function runGuarded(action: string, fn: () => void): void {
|
function runGuarded(action: string, fn: () => void): void {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const OVERLAY_HOSTED_MODALS = [
|
|||||||
'controller-select',
|
'controller-select',
|
||||||
'controller-debug',
|
'controller-debug',
|
||||||
'subtitle-sidebar',
|
'subtitle-sidebar',
|
||||||
|
'session-help',
|
||||||
] as const;
|
] as const;
|
||||||
export type OverlayHostedModal = (typeof OVERLAY_HOSTED_MODALS)[number];
|
export type OverlayHostedModal = (typeof OVERLAY_HOSTED_MODALS)[number];
|
||||||
|
|
||||||
@@ -111,6 +112,10 @@ export const IPC_CHANNELS = {
|
|||||||
playlistBrowserOpen: 'playlist-browser:open',
|
playlistBrowserOpen: 'playlist-browser:open',
|
||||||
keyboardModeToggleRequested: 'keyboard-mode-toggle:requested',
|
keyboardModeToggleRequested: 'keyboard-mode-toggle:requested',
|
||||||
lookupWindowToggleRequested: 'lookup-window-toggle:requested',
|
lookupWindowToggleRequested: 'lookup-window-toggle:requested',
|
||||||
|
sessionHelpOpen: 'session-help:open',
|
||||||
|
controllerSelectOpen: 'controller-select:open',
|
||||||
|
controllerDebugOpen: 'controller-debug:open',
|
||||||
|
subtitleSidebarToggle: 'subtitle-sidebar:toggle',
|
||||||
configHotReload: 'config:hot-reload',
|
configHotReload: 'config:hot-reload',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ export interface ShortcutsConfig {
|
|||||||
markAudioCard?: string | null;
|
markAudioCard?: string | null;
|
||||||
openRuntimeOptions?: string | null;
|
openRuntimeOptions?: string | null;
|
||||||
openJimaku?: string | null;
|
openJimaku?: string | null;
|
||||||
|
openSessionHelp?: string | null;
|
||||||
|
openControllerSelect?: string | null;
|
||||||
|
openControllerDebug?: string | null;
|
||||||
|
toggleSubtitleSidebar?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
|
|||||||
@@ -399,9 +399,13 @@ export interface ElectronAPI {
|
|||||||
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => Promise<RuntimeOptionApplyResult>;
|
cycleRuntimeOption: (id: RuntimeOptionId, direction: 1 | -1) => Promise<RuntimeOptionApplyResult>;
|
||||||
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
onRuntimeOptionsChanged: (callback: (options: RuntimeOptionState[]) => void) => void;
|
||||||
onOpenRuntimeOptions: (callback: () => void) => void;
|
onOpenRuntimeOptions: (callback: () => void) => void;
|
||||||
|
onOpenSessionHelp: (callback: () => void) => void;
|
||||||
|
onOpenControllerSelect: (callback: () => void) => void;
|
||||||
|
onOpenControllerDebug: (callback: () => void) => void;
|
||||||
onOpenJimaku: (callback: () => void) => void;
|
onOpenJimaku: (callback: () => void) => void;
|
||||||
onOpenYoutubeTrackPicker: (callback: (payload: YoutubePickerOpenPayload) => void) => void;
|
onOpenYoutubeTrackPicker: (callback: (payload: YoutubePickerOpenPayload) => void) => void;
|
||||||
onOpenPlaylistBrowser: (callback: () => void) => void;
|
onOpenPlaylistBrowser: (callback: () => void) => void;
|
||||||
|
onSubtitleSidebarToggle: (callback: () => void) => void;
|
||||||
onCancelYoutubeTrackPicker: (callback: () => void) => void;
|
onCancelYoutubeTrackPicker: (callback: () => void) => void;
|
||||||
onKeyboardModeToggleRequested: (callback: () => void) => void;
|
onKeyboardModeToggleRequested: (callback: () => void) => void;
|
||||||
onLookupWindowToggleRequested: (callback: () => void) => void;
|
onLookupWindowToggleRequested: (callback: () => void) => void;
|
||||||
@@ -427,7 +431,8 @@ export interface ElectronAPI {
|
|||||||
| 'kiku'
|
| 'kiku'
|
||||||
| 'controller-select'
|
| 'controller-select'
|
||||||
| 'controller-debug'
|
| 'controller-debug'
|
||||||
| 'subtitle-sidebar',
|
| 'subtitle-sidebar'
|
||||||
|
| 'session-help',
|
||||||
) => void;
|
) => void;
|
||||||
notifyOverlayModalOpened: (
|
notifyOverlayModalOpened: (
|
||||||
modal:
|
modal:
|
||||||
@@ -439,7 +444,8 @@ export interface ElectronAPI {
|
|||||||
| 'kiku'
|
| 'kiku'
|
||||||
| 'controller-select'
|
| 'controller-select'
|
||||||
| 'controller-debug'
|
| 'controller-debug'
|
||||||
| 'subtitle-sidebar',
|
| 'subtitle-sidebar'
|
||||||
|
| 'session-help',
|
||||||
) => void;
|
) => void;
|
||||||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
||||||
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ export type SessionActionId =
|
|||||||
| 'mineSentence'
|
| 'mineSentence'
|
||||||
| 'mineSentenceMultiple'
|
| 'mineSentenceMultiple'
|
||||||
| 'toggleSecondarySub'
|
| 'toggleSecondarySub'
|
||||||
|
| 'toggleSubtitleSidebar'
|
||||||
| 'markAudioCard'
|
| 'markAudioCard'
|
||||||
| 'openRuntimeOptions'
|
| 'openRuntimeOptions'
|
||||||
|
| 'openSessionHelp'
|
||||||
|
| 'openControllerSelect'
|
||||||
|
| 'openControllerDebug'
|
||||||
| 'openJimaku'
|
| 'openJimaku'
|
||||||
| 'openYoutubePicker'
|
| 'openYoutubePicker'
|
||||||
| 'openPlaylistBrowser'
|
| 'openPlaylistBrowser'
|
||||||
|
|||||||
Reference in New Issue
Block a user