mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-21 05:01:26 -07:00
feat(overlay): add optional subtitle selection modal (#265)
This commit is contained in:
1 parent
a7a302bdd0
commit
b0fb39a8f5
54 files changed
+1397
-52
No files matched your search
@@ -92,6 +92,7 @@ function createEmptyShortcuts(): ConfiguredShortcuts {
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openTsukihime: null,
|
||||
openSubtitleSelection: null,
|
||||
openSubtitleGeneration: null,
|
||||
openSessionHelp: null,
|
||||
openControllerSelect: null,
|
||||
@@ -2404,3 +2405,86 @@ test('stalled mpv discovery does not delay configured overlay controls', async (
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('session binding: g-s opens subtitle selection only after the complete sequence', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
sourcePath: 'shortcuts.openSubtitleSelection',
|
||||
originalKey: 'g-s',
|
||||
key: { code: 'KeyG-KeyS', modifiers: [] },
|
||||
actionType: 'session-action',
|
||||
actionId: 'openSubtitleSelection',
|
||||
},
|
||||
]);
|
||||
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS' });
|
||||
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
|
||||
assert.deepEqual(testGlobals.sessionActions, []);
|
||||
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS' });
|
||||
assert.deepEqual(testGlobals.sessionActions, [
|
||||
{ actionId: 'openSubtitleSelection', payload: undefined },
|
||||
]);
|
||||
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
|
||||
handlers.updateSessionBindings([]);
|
||||
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS' });
|
||||
assert.equal(testGlobals.sessionActions.length, 1);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('single-key actions run immediately even if a conflicting sequence reaches the renderer', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
sourcePath: 'sequence',
|
||||
originalKey: 'g-s',
|
||||
key: { code: 'KeyG-KeyS', modifiers: [] },
|
||||
actionType: 'session-action',
|
||||
actionId: 'openSubtitleSelection',
|
||||
},
|
||||
{
|
||||
sourcePath: 'single',
|
||||
originalKey: 'g',
|
||||
key: { code: 'KeyG', modifiers: [] },
|
||||
actionType: 'mpv-command',
|
||||
command: ['show-text', 'single'],
|
||||
},
|
||||
]);
|
||||
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
|
||||
assert.deepEqual(testGlobals.mpvCommands, [['show-text', 'single']]);
|
||||
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS' });
|
||||
assert.deepEqual(testGlobals.sessionActions, []);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('an unfinished built-in y chord cannot start a configured sequence', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
handlers.updateSessionBindings([
|
||||
{
|
||||
sourcePath: 'sequence',
|
||||
originalKey: 'g-s',
|
||||
key: { code: 'KeyG-KeyS', modifiers: [] },
|
||||
actionType: 'session-action',
|
||||
actionId: 'openSubtitleSelection',
|
||||
},
|
||||
]);
|
||||
testGlobals.dispatchKeydown({ key: 'y', code: 'KeyY' });
|
||||
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
|
||||
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS' });
|
||||
assert.deepEqual(testGlobals.sessionActions, []);
|
||||
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
|
||||
testGlobals.dispatchKeydown({ key: 's', code: 'KeyS' });
|
||||
assert.equal(testGlobals.sessionActions.length, 1);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
@@ -15,6 +15,7 @@ export function createKeyboardHandlers(
|
||||
handleRuntimeOptionsKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleCharacterDictionaryKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleSubsyncKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleSubtitleSelectionKeydown?: (e: KeyboardEvent) => boolean;
|
||||
handleSubtitleGenerationKeydown?: (e: KeyboardEvent) => boolean;
|
||||
handleKikuKeydown: (e: KeyboardEvent) => boolean;
|
||||
handleJimakuKeydown: (e: KeyboardEvent) => boolean;
|
||||
@@ -133,7 +134,10 @@ export function createKeyboardHandlers(
|
||||
updateConfiguredShortcuts(shortcuts, statsToggleKey, markWatchedKey);
|
||||
}
|
||||
|
||||
let pendingSequence: { prefix: string; expires: number } | null = null;
|
||||
|
||||
function updateSessionBindings(bindings: CompiledSessionBinding[]): void {
|
||||
pendingSequence = null;
|
||||
ctx.state.sessionBindings = bindings;
|
||||
ctx.state.sessionBindingMap = new Map(
|
||||
bindings.map((binding) => [keyEventToStringFromBinding(binding), binding]),
|
||||
@@ -1049,7 +1053,10 @@ export function createKeyboardHandlers(
|
||||
window.addEventListener('focus', () => {
|
||||
void importedMpvBindings.refresh();
|
||||
});
|
||||
window.addEventListener('blur', importedMpvBindings.releaseAll);
|
||||
window.addEventListener('blur', () => {
|
||||
pendingSequence = null;
|
||||
importedMpvBindings.releaseAll();
|
||||
});
|
||||
window.addEventListener('beforeunload', () => {
|
||||
clearTimeout(lateScriptRefresh);
|
||||
importedMpvBindings.dispose();
|
||||
@@ -1103,6 +1110,13 @@ export function createKeyboardHandlers(
|
||||
);
|
||||
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const sequence = pendingSequence;
|
||||
pendingSequence = null;
|
||||
if (ctx.state.subtitleSelectionModalOpen) {
|
||||
pendingSequence = null;
|
||||
options.handleSubtitleSelectionKeydown?.(e);
|
||||
return;
|
||||
}
|
||||
if (ctx.state.subtitleGenerationModalOpen) {
|
||||
options.handleSubtitleGenerationKeydown?.(e);
|
||||
return;
|
||||
@@ -1187,6 +1201,7 @@ export function createKeyboardHandlers(
|
||||
}
|
||||
|
||||
if (isTextEntryTarget(e.target)) {
|
||||
pendingSequence = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1194,6 +1209,16 @@ export function createKeyboardHandlers(
|
||||
return;
|
||||
}
|
||||
|
||||
const sequenceKey = keyEventToString(e);
|
||||
if (sequence && !ctx.state.chordPending && Date.now() <= sequence.expires && !e.repeat) {
|
||||
const binding = ctx.state.sessionBindingMap.get(`${sequence.prefix}-${sequenceKey}`);
|
||||
if (binding) {
|
||||
e.preventDefault();
|
||||
dispatchSessionBinding(binding);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isStatsOverlayToggle(e)) {
|
||||
e.preventDefault();
|
||||
window.electronAPI.toggleStatsOverlay();
|
||||
@@ -1276,6 +1301,16 @@ export function createKeyboardHandlers(
|
||||
dispatchSessionBinding(binding);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!e.repeat &&
|
||||
ctx.state.sessionBindings.some((binding) =>
|
||||
keyEventToStringFromBinding(binding).startsWith(`${sequenceKey}-`),
|
||||
)
|
||||
) {
|
||||
pendingSequence = { prefix: sequenceKey, expires: Date.now() + 1000 };
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
keyboardConfigLoaded &&
|
||||
!ctx.state.playlistBrowserModalOpen &&
|
||||
|
||||
Reference in new issue
Block a user