mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-12 04:19:25 -07:00
feat: wire session bindings through main, ipc, and cli runtime
This commit is contained in:
@@ -3,6 +3,7 @@ import test from 'node:test';
|
||||
|
||||
import { createKeyboardHandlers } from './keyboard.js';
|
||||
import { createRendererState } from '../state.js';
|
||||
import type { CompiledSessionBinding } from '../../types';
|
||||
import { YOMITAN_POPUP_COMMAND_EVENT, YOMITAN_POPUP_HIDDEN_EVENT } from '../yomitan-popup.js';
|
||||
|
||||
type CommandEventDetail = {
|
||||
@@ -50,6 +51,8 @@ function installKeyboardTestGlobals() {
|
||||
const windowListeners = new Map<string, Array<(event: unknown) => void>>();
|
||||
const commandEvents: CommandEventDetail[] = [];
|
||||
const mpvCommands: Array<Array<string | number>> = [];
|
||||
const sessionActions: Array<{ actionId: string; payload?: unknown }> = [];
|
||||
let sessionBindings: CompiledSessionBinding[] = [];
|
||||
let playbackPausedResponse: boolean | null = false;
|
||||
let statsToggleKey = 'Backquote';
|
||||
let markWatchedKey = 'KeyW';
|
||||
@@ -153,10 +156,14 @@ function installKeyboardTestGlobals() {
|
||||
},
|
||||
electronAPI: {
|
||||
getKeybindings: async () => [],
|
||||
getSessionBindings: async () => sessionBindings,
|
||||
getConfiguredShortcuts: async () => configuredShortcuts,
|
||||
sendMpvCommand: (command: Array<string | number>) => {
|
||||
mpvCommands.push(command);
|
||||
},
|
||||
dispatchSessionAction: async (actionId: string, payload?: unknown) => {
|
||||
sessionActions.push({ actionId, payload });
|
||||
},
|
||||
getPlaybackPaused: async () => playbackPausedResponse,
|
||||
getStatsToggleKey: async () => statsToggleKey,
|
||||
getMarkWatchedKey: async () => markWatchedKey,
|
||||
@@ -273,6 +280,7 @@ function installKeyboardTestGlobals() {
|
||||
return {
|
||||
commandEvents,
|
||||
mpvCommands,
|
||||
sessionActions,
|
||||
overlay,
|
||||
overlayFocusCalls,
|
||||
focusMainWindowCalls: () => focusMainWindowCalls,
|
||||
@@ -292,6 +300,9 @@ function installKeyboardTestGlobals() {
|
||||
setConfiguredShortcuts: (value: typeof configuredShortcuts) => {
|
||||
configuredShortcuts = value;
|
||||
},
|
||||
setSessionBindings: (value: CompiledSessionBinding[]) => {
|
||||
sessionBindings = value;
|
||||
},
|
||||
setMarkActiveVideoWatchedResult: (value: boolean) => {
|
||||
markActiveVideoWatchedResult = value;
|
||||
},
|
||||
@@ -521,13 +532,19 @@ test('popup-visible mpv keybindings still fire for bound keys', async () => {
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateKeybindings([
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
key: 'Space',
|
||||
sourcePath: 'keybindings[0].key',
|
||||
originalKey: 'Space',
|
||||
key: { code: 'Space', modifiers: [] },
|
||||
actionType: 'mpv-command',
|
||||
command: ['cycle', 'pause'],
|
||||
},
|
||||
{
|
||||
key: 'KeyQ',
|
||||
sourcePath: 'keybindings[1].key',
|
||||
originalKey: 'KeyQ',
|
||||
key: { code: 'KeyQ', modifiers: [] },
|
||||
actionType: 'mpv-command',
|
||||
command: ['quit'],
|
||||
},
|
||||
] as never);
|
||||
@@ -549,9 +566,12 @@ test('paused configured subtitle-jump keybinding re-applies pause after backward
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateKeybindings([
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
key: 'Shift+KeyH',
|
||||
sourcePath: 'keybindings[0].key',
|
||||
originalKey: 'Shift+KeyH',
|
||||
key: { code: 'KeyH', modifiers: ['shift'] },
|
||||
actionType: 'mpv-command',
|
||||
command: ['sub-seek', -1],
|
||||
},
|
||||
] as never);
|
||||
@@ -574,9 +594,12 @@ test('configured subtitle-jump keybinding preserves pause when pause state is un
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateKeybindings([
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
key: 'Shift+KeyH',
|
||||
sourcePath: 'keybindings[0].key',
|
||||
originalKey: 'Shift+KeyH',
|
||||
key: { code: 'KeyH', modifiers: ['shift'] },
|
||||
actionType: 'mpv-command',
|
||||
command: ['sub-seek', -1],
|
||||
},
|
||||
] as never);
|
||||
@@ -763,13 +786,19 @@ test('youtube picker: unhandled keys still dispatch mpv keybindings', async () =
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateKeybindings([
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
key: 'Space',
|
||||
sourcePath: 'keybindings[0].key',
|
||||
originalKey: 'Space',
|
||||
key: { code: 'Space', modifiers: [] },
|
||||
actionType: 'mpv-command',
|
||||
command: ['cycle', 'pause'],
|
||||
},
|
||||
{
|
||||
key: 'KeyQ',
|
||||
sourcePath: 'keybindings[1].key',
|
||||
originalKey: 'KeyQ',
|
||||
key: { code: 'KeyQ', modifiers: [] },
|
||||
actionType: 'mpv-command',
|
||||
command: ['quit'],
|
||||
},
|
||||
] as never);
|
||||
@@ -785,46 +814,72 @@ test('youtube picker: unhandled keys still dispatch mpv keybindings', async () =
|
||||
}
|
||||
});
|
||||
|
||||
test('linux overlay shortcut: Ctrl+Alt+S dispatches subsync special command locally', async () => {
|
||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
test('session binding: Ctrl+Alt+S dispatches subsync action locally', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
ctx.platform.isLinuxPlatform = true;
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
sourcePath: 'shortcuts.triggerSubsync',
|
||||
originalKey: 'Ctrl+Alt+S',
|
||||
key: { code: 'KeyS', modifiers: ['ctrl', 'alt'] },
|
||||
actionType: 'session-action',
|
||||
actionId: 'triggerSubsync',
|
||||
},
|
||||
] as never);
|
||||
|
||||
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS', ctrlKey: true, altKey: true });
|
||||
|
||||
assert.deepEqual(testGlobals.mpvCommands, [['__subsync-trigger']]);
|
||||
assert.deepEqual(testGlobals.sessionActions, [{ actionId: 'triggerSubsync', payload: undefined }]);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('linux overlay shortcut: Ctrl+Shift+J dispatches jimaku special command locally', async () => {
|
||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
test('session binding: Ctrl+Shift+J dispatches jimaku action locally', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
ctx.platform.isLinuxPlatform = true;
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
sourcePath: 'shortcuts.openJimaku',
|
||||
originalKey: 'Ctrl+Shift+J',
|
||||
key: { code: 'KeyJ', modifiers: ['ctrl', 'shift'] },
|
||||
actionType: 'session-action',
|
||||
actionId: 'openJimaku',
|
||||
},
|
||||
] as never);
|
||||
|
||||
testGlobals.dispatchKeydown({ key: 'J', code: 'KeyJ', ctrlKey: true, shiftKey: true });
|
||||
|
||||
assert.deepEqual(testGlobals.mpvCommands, [['__jimaku-open']]);
|
||||
assert.deepEqual(testGlobals.sessionActions, [{ actionId: 'openJimaku', payload: undefined }]);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('linux overlay shortcut: CommandOrControl+Shift+O dispatches runtime options locally', async () => {
|
||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
test('session binding: Ctrl+Shift+O dispatches runtime options locally', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
try {
|
||||
ctx.platform.isLinuxPlatform = true;
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
sourcePath: 'shortcuts.openRuntimeOptions',
|
||||
originalKey: 'CommandOrControl+Shift+O',
|
||||
key: { code: 'KeyO', modifiers: ['ctrl', 'shift'] },
|
||||
actionType: 'session-action',
|
||||
actionId: 'openRuntimeOptions',
|
||||
},
|
||||
] as never);
|
||||
|
||||
testGlobals.dispatchKeydown({ key: 'O', code: 'KeyO', ctrlKey: true, shiftKey: true });
|
||||
|
||||
assert.deepEqual(testGlobals.mpvCommands, [['__runtime-options-open']]);
|
||||
assert.deepEqual(testGlobals.sessionActions, [
|
||||
{ actionId: 'openRuntimeOptions', payload: undefined },
|
||||
]);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { SPECIAL_COMMANDS } from '../../config/definitions';
|
||||
import type { Keybinding, ShortcutsConfig } from '../../types';
|
||||
import type { CompiledSessionBinding, ShortcutsConfig } from '../../types';
|
||||
import type { RendererContext } from '../context';
|
||||
import {
|
||||
YOMITAN_POPUP_HOST_SELECTOR,
|
||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||
YOMITAN_POPUP_SHOWN_EVENT,
|
||||
YOMITAN_POPUP_COMMAND_EVENT,
|
||||
@@ -37,11 +35,16 @@ export function createKeyboardHandlers(
|
||||
// Timeout for the modal chord capture window (e.g. Y followed by H/K).
|
||||
const CHORD_TIMEOUT_MS = 1000;
|
||||
const KEYBOARD_SELECTED_WORD_CLASS = 'keyboard-selected';
|
||||
const linuxOverlayShortcutCommands = new Map<string, (string | number)[]>();
|
||||
let pendingSelectionAnchorAfterSubtitleSeek: 'start' | 'end' | null = null;
|
||||
let pendingLookupRefreshAfterSubtitleSeek = false;
|
||||
let resetSelectionToStartOnNextSubtitleSync = false;
|
||||
let lookupScanFallbackTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pendingNumericSelection:
|
||||
| {
|
||||
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple';
|
||||
timeout: ReturnType<typeof setTimeout> | null;
|
||||
}
|
||||
| null = null;
|
||||
|
||||
const CHORD_MAP = new Map<
|
||||
string,
|
||||
@@ -62,9 +65,6 @@ export function createKeyboardHandlers(
|
||||
if (target.closest('.modal')) return true;
|
||||
if (ctx.dom.subtitleContainer.contains(target)) return true;
|
||||
if (isYomitanPopupIframe(target)) return true;
|
||||
if (target.closest && target.closest(YOMITAN_POPUP_HOST_SELECTOR)) {
|
||||
return true;
|
||||
}
|
||||
if (target.closest && target.closest('iframe.yomitan-popup, iframe[id^="yomitan-popup"]'))
|
||||
return true;
|
||||
return false;
|
||||
@@ -80,115 +80,117 @@ export function createKeyboardHandlers(
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
function acceleratorToKeyToken(token: string): string | null {
|
||||
const normalized = token.trim();
|
||||
if (!normalized) return null;
|
||||
if (/^[a-z]$/i.test(normalized)) {
|
||||
return `Key${normalized.toUpperCase()}`;
|
||||
}
|
||||
if (/^[0-9]$/.test(normalized)) {
|
||||
return `Digit${normalized}`;
|
||||
}
|
||||
const exactMap: Record<string, string> = {
|
||||
space: 'Space',
|
||||
tab: 'Tab',
|
||||
enter: 'Enter',
|
||||
return: 'Enter',
|
||||
esc: 'Escape',
|
||||
escape: 'Escape',
|
||||
up: 'ArrowUp',
|
||||
down: 'ArrowDown',
|
||||
left: 'ArrowLeft',
|
||||
right: 'ArrowRight',
|
||||
backspace: 'Backspace',
|
||||
delete: 'Delete',
|
||||
slash: 'Slash',
|
||||
backslash: 'Backslash',
|
||||
minus: 'Minus',
|
||||
plus: 'Equal',
|
||||
equal: 'Equal',
|
||||
comma: 'Comma',
|
||||
period: 'Period',
|
||||
quote: 'Quote',
|
||||
semicolon: 'Semicolon',
|
||||
bracketleft: 'BracketLeft',
|
||||
bracketright: 'BracketRight',
|
||||
backquote: 'Backquote',
|
||||
};
|
||||
const lower = normalized.toLowerCase();
|
||||
if (exactMap[lower]) return exactMap[lower];
|
||||
if (/^key[a-z]$/i.test(normalized) || /^digit[0-9]$/i.test(normalized)) {
|
||||
return normalized[0]!.toUpperCase() + normalized.slice(1);
|
||||
}
|
||||
if (/^arrow(?:up|down|left|right)$/i.test(normalized)) {
|
||||
return normalized[0]!.toUpperCase() + normalized.slice(1);
|
||||
}
|
||||
if (/^f\d{1,2}$/i.test(normalized)) {
|
||||
return normalized.toUpperCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function acceleratorToKeyString(accelerator: string): string | null {
|
||||
const normalized = accelerator.replace(/\s+/g, '').replace(/cmdorctrl/gi, 'CommandOrControl');
|
||||
if (!normalized) return null;
|
||||
const parts = normalized.split('+').filter(Boolean);
|
||||
const keyToken = parts.pop();
|
||||
if (!keyToken) return null;
|
||||
|
||||
const eventParts: string[] = [];
|
||||
for (const modifier of parts) {
|
||||
const lower = modifier.toLowerCase();
|
||||
if (lower === 'ctrl' || lower === 'control') {
|
||||
eventParts.push('Ctrl');
|
||||
continue;
|
||||
}
|
||||
if (lower === 'alt' || lower === 'option') {
|
||||
eventParts.push('Alt');
|
||||
continue;
|
||||
}
|
||||
if (lower === 'shift') {
|
||||
eventParts.push('Shift');
|
||||
continue;
|
||||
}
|
||||
if (lower === 'meta' || lower === 'super' || lower === 'command' || lower === 'cmd') {
|
||||
eventParts.push('Meta');
|
||||
continue;
|
||||
}
|
||||
if (lower === 'commandorcontrol') {
|
||||
eventParts.push(ctx.platform.isMacOSPlatform ? 'Meta' : 'Ctrl');
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedKey = acceleratorToKeyToken(keyToken);
|
||||
if (!normalizedKey) return null;
|
||||
eventParts.push(normalizedKey);
|
||||
return eventParts.join('+');
|
||||
}
|
||||
|
||||
function updateConfiguredShortcuts(shortcuts: Required<ShortcutsConfig>): void {
|
||||
linuxOverlayShortcutCommands.clear();
|
||||
const bindings: Array<[string | null, (string | number)[]]> = [
|
||||
[shortcuts.triggerSubsync, [SPECIAL_COMMANDS.SUBSYNC_TRIGGER]],
|
||||
[shortcuts.openRuntimeOptions, [SPECIAL_COMMANDS.RUNTIME_OPTIONS_OPEN]],
|
||||
[shortcuts.openJimaku, [SPECIAL_COMMANDS.JIMAKU_OPEN]],
|
||||
];
|
||||
|
||||
for (const [accelerator, command] of bindings) {
|
||||
if (!accelerator) continue;
|
||||
const keyString = acceleratorToKeyString(accelerator);
|
||||
if (keyString) {
|
||||
linuxOverlayShortcutCommands.set(keyString, command);
|
||||
}
|
||||
}
|
||||
ctx.state.sessionActionTimeoutMs = shortcuts.multiCopyTimeoutMs;
|
||||
}
|
||||
|
||||
async function refreshConfiguredShortcuts(): Promise<void> {
|
||||
updateConfiguredShortcuts(await window.electronAPI.getConfiguredShortcuts());
|
||||
}
|
||||
|
||||
function updateSessionBindings(bindings: CompiledSessionBinding[]): void {
|
||||
ctx.state.sessionBindings = bindings;
|
||||
ctx.state.sessionBindingMap = new Map(
|
||||
bindings.map((binding) => [keyEventToStringFromBinding(binding), binding]),
|
||||
);
|
||||
}
|
||||
|
||||
function keyEventToStringFromBinding(binding: CompiledSessionBinding): string {
|
||||
const parts: string[] = [];
|
||||
for (const modifier of binding.key.modifiers) {
|
||||
if (modifier === 'ctrl') parts.push('Ctrl');
|
||||
else if (modifier === 'alt') parts.push('Alt');
|
||||
else if (modifier === 'shift') parts.push('Shift');
|
||||
else if (modifier === 'meta') parts.push('Meta');
|
||||
}
|
||||
parts.push(binding.key.code);
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
function isTextEntryTarget(target: EventTarget | null): boolean {
|
||||
if (!target || typeof target !== 'object' || !('closest' in target)) return false;
|
||||
const element = target as { closest: (selector: string) => unknown };
|
||||
if (element.closest('[contenteditable="true"]')) return true;
|
||||
return Boolean(element.closest('input, textarea, select'));
|
||||
}
|
||||
|
||||
function showSessionSelectionMessage(message: string): void {
|
||||
window.electronAPI.sendMpvCommand(['show-text', message, '3000']);
|
||||
}
|
||||
|
||||
function cancelPendingNumericSelection(showCancelled: boolean): void {
|
||||
if (!pendingNumericSelection) return;
|
||||
if (pendingNumericSelection.timeout !== null) {
|
||||
clearTimeout(pendingNumericSelection.timeout);
|
||||
}
|
||||
pendingNumericSelection = null;
|
||||
if (showCancelled) {
|
||||
showSessionSelectionMessage('Cancelled');
|
||||
}
|
||||
}
|
||||
|
||||
function startPendingNumericSelection(
|
||||
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple',
|
||||
): void {
|
||||
cancelPendingNumericSelection(false);
|
||||
const timeoutMessage = actionId === 'copySubtitleMultiple' ? 'Copy timeout' : 'Mine timeout';
|
||||
const promptMessage =
|
||||
actionId === 'copySubtitleMultiple'
|
||||
? 'Copy how many lines? Press 1-9 (Esc to cancel)'
|
||||
: 'Mine how many lines? Press 1-9 (Esc to cancel)';
|
||||
pendingNumericSelection = {
|
||||
actionId,
|
||||
timeout: setTimeout(() => {
|
||||
pendingNumericSelection = null;
|
||||
showSessionSelectionMessage(timeoutMessage);
|
||||
}, ctx.state.sessionActionTimeoutMs),
|
||||
};
|
||||
showSessionSelectionMessage(promptMessage);
|
||||
}
|
||||
|
||||
function beginSessionNumericSelection(
|
||||
actionId: 'copySubtitleMultiple' | 'mineSentenceMultiple',
|
||||
): void {
|
||||
startPendingNumericSelection(actionId);
|
||||
}
|
||||
|
||||
function handlePendingNumericSelection(e: KeyboardEvent): boolean {
|
||||
if (!pendingNumericSelection) return false;
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cancelPendingNumericSelection(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!/^[1-9]$/.test(e.key) || e.ctrlKey || e.metaKey || e.altKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const count = Number(e.key);
|
||||
const actionId = pendingNumericSelection.actionId;
|
||||
cancelPendingNumericSelection(false);
|
||||
void window.electronAPI.dispatchSessionAction(actionId, { count });
|
||||
return true;
|
||||
}
|
||||
|
||||
function dispatchSessionBinding(binding: CompiledSessionBinding): void {
|
||||
if (
|
||||
binding.actionType === 'session-action' &&
|
||||
(binding.actionId === 'copySubtitleMultiple' || binding.actionId === 'mineSentenceMultiple')
|
||||
) {
|
||||
startPendingNumericSelection(binding.actionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (binding.actionType === 'mpv-command') {
|
||||
dispatchConfiguredMpvCommand(binding.command);
|
||||
return;
|
||||
}
|
||||
|
||||
void window.electronAPI.dispatchSessionAction(binding.actionId, binding.payload);
|
||||
}
|
||||
|
||||
function dispatchYomitanPopupKeydown(
|
||||
key: string,
|
||||
code: string,
|
||||
@@ -512,7 +514,7 @@ export function createKeyboardHandlers(
|
||||
clientY: number,
|
||||
modifiers: ScanModifierState = {},
|
||||
): void {
|
||||
if (typeof PointerEvent !== 'undefined') {
|
||||
if (typeof PointerEvent === 'function') {
|
||||
const pointerEventInit = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
@@ -535,23 +537,25 @@ export function createKeyboardHandlers(
|
||||
target.dispatchEvent(new PointerEvent('pointerup', pointerEventInit));
|
||||
}
|
||||
|
||||
const mouseEventInit = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
clientX,
|
||||
clientY,
|
||||
button: 0,
|
||||
buttons: 0,
|
||||
shiftKey: modifiers.shiftKey ?? false,
|
||||
ctrlKey: modifiers.ctrlKey ?? false,
|
||||
altKey: modifiers.altKey ?? false,
|
||||
metaKey: modifiers.metaKey ?? false,
|
||||
} satisfies MouseEventInit;
|
||||
target.dispatchEvent(new MouseEvent('mousemove', mouseEventInit));
|
||||
target.dispatchEvent(new MouseEvent('mousedown', { ...mouseEventInit, buttons: 1 }));
|
||||
target.dispatchEvent(new MouseEvent('mouseup', mouseEventInit));
|
||||
target.dispatchEvent(new MouseEvent('click', mouseEventInit));
|
||||
if (typeof MouseEvent === 'function') {
|
||||
const mouseEventInit = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
clientX,
|
||||
clientY,
|
||||
button: 0,
|
||||
buttons: 0,
|
||||
shiftKey: modifiers.shiftKey ?? false,
|
||||
ctrlKey: modifiers.ctrlKey ?? false,
|
||||
altKey: modifiers.altKey ?? false,
|
||||
metaKey: modifiers.metaKey ?? false,
|
||||
} satisfies MouseEventInit;
|
||||
target.dispatchEvent(new MouseEvent('mousemove', mouseEventInit));
|
||||
target.dispatchEvent(new MouseEvent('mousedown', { ...mouseEventInit, buttons: 1 }));
|
||||
target.dispatchEvent(new MouseEvent('mouseup', mouseEventInit));
|
||||
target.dispatchEvent(new MouseEvent('click', mouseEventInit));
|
||||
}
|
||||
}
|
||||
|
||||
function emitLookupScanFallback(target: Element, clientX: number, clientY: number): void {
|
||||
@@ -824,7 +828,7 @@ export function createKeyboardHandlers(
|
||||
if (modifierOnlyCodes.has(e.code)) return false;
|
||||
|
||||
const keyString = keyEventToString(e);
|
||||
if (ctx.state.keybindingsMap.has(keyString)) {
|
||||
if (ctx.state.sessionBindingMap.has(keyString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -850,7 +854,7 @@ export function createKeyboardHandlers(
|
||||
fallbackUnavailable: boolean;
|
||||
} {
|
||||
const firstChoice = 'KeyH';
|
||||
if (!ctx.state.keybindingsMap.has('KeyH')) {
|
||||
if (!ctx.state.sessionBindingMap.has('KeyH')) {
|
||||
return {
|
||||
bindingKey: firstChoice,
|
||||
fallbackUsed: false,
|
||||
@@ -858,18 +862,18 @@ export function createKeyboardHandlers(
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.state.keybindingsMap.has('KeyK')) {
|
||||
if (!ctx.state.sessionBindingMap.has('KeyK')) {
|
||||
return {
|
||||
bindingKey: 'KeyK',
|
||||
fallbackUsed: true,
|
||||
fallbackUnavailable: true,
|
||||
fallbackUnavailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
bindingKey: 'KeyK',
|
||||
fallbackUsed: true,
|
||||
fallbackUnavailable: false,
|
||||
fallbackUnavailable: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -894,13 +898,13 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
|
||||
async function setupMpvInputForwarding(): Promise<void> {
|
||||
const [keybindings, shortcuts, statsToggleKey, markWatchedKey] = await Promise.all([
|
||||
window.electronAPI.getKeybindings(),
|
||||
const [sessionBindings, shortcuts, statsToggleKey, markWatchedKey] = await Promise.all([
|
||||
window.electronAPI.getSessionBindings(),
|
||||
window.electronAPI.getConfiguredShortcuts(),
|
||||
window.electronAPI.getStatsToggleKey(),
|
||||
window.electronAPI.getMarkWatchedKey(),
|
||||
]);
|
||||
updateKeybindings(keybindings);
|
||||
updateSessionBindings(sessionBindings);
|
||||
updateConfiguredShortcuts(shortcuts);
|
||||
ctx.state.statsToggleKey = statsToggleKey;
|
||||
ctx.state.markWatchedKey = markWatchedKey;
|
||||
@@ -1010,6 +1014,14 @@ export function createKeyboardHandlers(
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTextEntryTarget(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handlePendingNumericSelection(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isStatsOverlayToggle(e)) {
|
||||
e.preventDefault();
|
||||
window.electronAPI.toggleStatsOverlay();
|
||||
@@ -1099,19 +1111,10 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
|
||||
const keyString = keyEventToString(e);
|
||||
const linuxOverlayCommand = ctx.platform.isLinuxPlatform
|
||||
? linuxOverlayShortcutCommands.get(keyString)
|
||||
: undefined;
|
||||
if (linuxOverlayCommand) {
|
||||
const binding = ctx.state.sessionBindingMap.get(keyString);
|
||||
if (binding) {
|
||||
e.preventDefault();
|
||||
dispatchConfiguredMpvCommand(linuxOverlayCommand);
|
||||
return;
|
||||
}
|
||||
const command = ctx.state.keybindingsMap.get(keyString);
|
||||
|
||||
if (command) {
|
||||
e.preventDefault();
|
||||
dispatchConfiguredMpvCommand(command);
|
||||
dispatchSessionBinding(binding);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1129,19 +1132,11 @@ export function createKeyboardHandlers(
|
||||
});
|
||||
}
|
||||
|
||||
function updateKeybindings(keybindings: Keybinding[]): void {
|
||||
ctx.state.keybindingsMap = new Map();
|
||||
for (const binding of keybindings) {
|
||||
if (binding.command) {
|
||||
ctx.state.keybindingsMap.set(binding.key, binding.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
beginSessionNumericSelection,
|
||||
setupMpvInputForwarding,
|
||||
refreshConfiguredShortcuts,
|
||||
updateKeybindings,
|
||||
updateSessionBindings,
|
||||
syncKeyboardTokenSelection,
|
||||
handleSubtitleContentUpdated,
|
||||
handleKeyboardModeToggleRequested,
|
||||
|
||||
@@ -628,7 +628,7 @@ async function init(): Promise<void> {
|
||||
});
|
||||
window.electronAPI.onConfigHotReload((payload: ConfigHotReloadPayload) => {
|
||||
runGuarded('config:hot-reload', () => {
|
||||
keyboardHandlers.updateKeybindings(payload.keybindings);
|
||||
keyboardHandlers.updateSessionBindings(payload.sessionBindings);
|
||||
void keyboardHandlers.refreshConfiguredShortcuts();
|
||||
subtitleRenderer.applySubtitleStyle(payload.subtitleStyle);
|
||||
subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
CompiledSessionBinding,
|
||||
PlaylistBrowserSnapshot,
|
||||
ControllerButtonSnapshot,
|
||||
ControllerDeviceInfo,
|
||||
@@ -116,7 +117,9 @@ export type RendererState = {
|
||||
frequencyDictionaryBand4Color: string;
|
||||
frequencyDictionaryBand5Color: string;
|
||||
|
||||
keybindingsMap: Map<string, (string | number)[]>;
|
||||
sessionBindings: CompiledSessionBinding[];
|
||||
sessionBindingMap: Map<string, CompiledSessionBinding>;
|
||||
sessionActionTimeoutMs: number;
|
||||
statsToggleKey: string;
|
||||
markWatchedKey: string;
|
||||
chordPending: boolean;
|
||||
@@ -219,7 +222,9 @@ export function createRendererState(): RendererState {
|
||||
frequencyDictionaryBand4Color: '#8bd5ca',
|
||||
frequencyDictionaryBand5Color: '#8aadf4',
|
||||
|
||||
keybindingsMap: new Map(),
|
||||
sessionBindings: [],
|
||||
sessionBindingMap: new Map(),
|
||||
sessionActionTimeoutMs: 3000,
|
||||
statsToggleKey: 'Backquote',
|
||||
markWatchedKey: 'KeyW',
|
||||
chordPending: false,
|
||||
|
||||
@@ -32,9 +32,9 @@ export function isYomitanPopupIframe(element: Element | null): boolean {
|
||||
return hasModernPopupClass || hasLegacyPopupId;
|
||||
}
|
||||
|
||||
export function hasYomitanPopupIframe(root: ParentNode = document): boolean {
|
||||
export function hasYomitanPopupIframe(root: ParentNode | null | undefined = document): boolean {
|
||||
return (
|
||||
typeof root.querySelector === 'function' &&
|
||||
typeof root?.querySelector === 'function' &&
|
||||
(root.querySelector(YOMITAN_POPUP_IFRAME_SELECTOR) !== null ||
|
||||
root.querySelector(YOMITAN_POPUP_HOST_SELECTOR) !== null)
|
||||
);
|
||||
@@ -58,14 +58,17 @@ function isMarkedVisiblePopupHost(element: Element): boolean {
|
||||
return element.getAttribute(YOMITAN_POPUP_VISIBLE_ATTRIBUTE) === 'true';
|
||||
}
|
||||
|
||||
function queryPopupElements<T extends Element>(root: ParentNode, selector: string): T[] {
|
||||
if (typeof root.querySelectorAll !== 'function') {
|
||||
function queryPopupElements<T extends Element>(
|
||||
root: ParentNode | null | undefined,
|
||||
selector: string,
|
||||
): T[] {
|
||||
if (typeof root?.querySelectorAll !== 'function') {
|
||||
return [];
|
||||
}
|
||||
return Array.from(root.querySelectorAll<T>(selector));
|
||||
}
|
||||
|
||||
export function isYomitanPopupVisible(root: ParentNode = document): boolean {
|
||||
export function isYomitanPopupVisible(root: ParentNode | null | undefined = document): boolean {
|
||||
const visiblePopupHosts = queryPopupElements<HTMLElement>(root, YOMITAN_POPUP_VISIBLE_HOST_SELECTOR);
|
||||
if (visiblePopupHosts.length > 0) {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user