mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 18:22:41 -08:00
feat(keybindings): cycle subtitle tracks on j/J with mpv-style OSD
This commit is contained in:
@@ -38,6 +38,8 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
| Shortcut | Action |
|
| Shortcut | Action |
|
||||||
| -------------------- | -------------------------------------------------- |
|
| -------------------- | -------------------------------------------------- |
|
||||||
| `Space` | Toggle mpv pause |
|
| `Space` | Toggle mpv pause |
|
||||||
|
| `J` | Cycle primary subtitle track |
|
||||||
|
| `Shift+J` | Cycle secondary subtitle track |
|
||||||
| `ArrowRight` | Seek forward 5 seconds |
|
| `ArrowRight` | Seek forward 5 seconds |
|
||||||
| `ArrowLeft` | Seek backward 5 seconds |
|
| `ArrowLeft` | Seek backward 5 seconds |
|
||||||
| `ArrowUp` | Seek forward 60 seconds |
|
| `ArrowUp` | Seek forward 60 seconds |
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CONFIG_OPTION_REGISTRY,
|
CONFIG_OPTION_REGISTRY,
|
||||||
CONFIG_TEMPLATE_SECTIONS,
|
CONFIG_TEMPLATE_SECTIONS,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
|
DEFAULT_KEYBINDINGS,
|
||||||
RUNTIME_OPTION_REGISTRY,
|
RUNTIME_OPTION_REGISTRY,
|
||||||
} from '../definitions';
|
} from '../definitions';
|
||||||
import { buildCoreConfigOptionRegistry } from './options-core';
|
import { buildCoreConfigOptionRegistry } from './options-core';
|
||||||
@@ -59,3 +60,9 @@ test('domain registry builders each contribute entries to composed registry', ()
|
|||||||
assert.ok(entries.some((entry) => composedPaths.has(entry.path)));
|
assert.ok(entries.some((entry) => composedPaths.has(entry.path)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('default keybindings include primary and secondary subtitle track cycling on J keys', () => {
|
||||||
|
const keybindingMap = new Map(DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command]));
|
||||||
|
assert.deepEqual(keybindingMap.get('KeyJ'), ['cycle', 'sid']);
|
||||||
|
assert.deepEqual(keybindingMap.get('Shift+KeyJ'), ['cycle', 'secondary-sid']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export const SPECIAL_COMMANDS = {
|
|||||||
|
|
||||||
export const DEFAULT_KEYBINDINGS: NonNullable<ResolvedConfig['keybindings']> = [
|
export const DEFAULT_KEYBINDINGS: NonNullable<ResolvedConfig['keybindings']> = [
|
||||||
{ key: 'Space', command: ['cycle', 'pause'] },
|
{ key: 'Space', command: ['cycle', 'pause'] },
|
||||||
|
{ key: 'KeyJ', command: ['cycle', 'sid'] },
|
||||||
|
{ key: 'Shift+KeyJ', command: ['cycle', 'secondary-sid'] },
|
||||||
{ key: 'ArrowRight', command: ['seek', 5] },
|
{ key: 'ArrowRight', command: ['seek', 5] },
|
||||||
{ key: 'ArrowLeft', command: ['seek', -5] },
|
{ key: 'ArrowLeft', command: ['seek', -5] },
|
||||||
{ key: 'ArrowUp', command: ['seek', 60] },
|
{ key: 'ArrowUp', command: ['seek', 60] },
|
||||||
|
|||||||
78
src/core/services/ipc-command.test.ts
Normal file
78
src/core/services/ipc-command.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { handleMpvCommandFromIpc } from './ipc-command';
|
||||||
|
|
||||||
|
function createOptions(overrides: Partial<Parameters<typeof handleMpvCommandFromIpc>[1]> = {}) {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const sentCommands: (string | number)[][] = [];
|
||||||
|
const osd: string[] = [];
|
||||||
|
const options: Parameters<typeof handleMpvCommandFromIpc>[1] = {
|
||||||
|
specialCommands: {
|
||||||
|
SUBSYNC_TRIGGER: '__subsync-trigger',
|
||||||
|
RUNTIME_OPTIONS_OPEN: '__runtime-options-open',
|
||||||
|
RUNTIME_OPTION_CYCLE_PREFIX: '__runtime-option-cycle:',
|
||||||
|
REPLAY_SUBTITLE: '__replay-subtitle',
|
||||||
|
PLAY_NEXT_SUBTITLE: '__play-next-subtitle',
|
||||||
|
},
|
||||||
|
triggerSubsyncFromConfig: () => {
|
||||||
|
calls.push('subsync');
|
||||||
|
},
|
||||||
|
openRuntimeOptionsPalette: () => {
|
||||||
|
calls.push('runtime-options');
|
||||||
|
},
|
||||||
|
runtimeOptionsCycle: () => ({ ok: true }),
|
||||||
|
showMpvOsd: (text) => {
|
||||||
|
osd.push(text);
|
||||||
|
},
|
||||||
|
mpvReplaySubtitle: () => {
|
||||||
|
calls.push('replay');
|
||||||
|
},
|
||||||
|
mpvPlayNextSubtitle: () => {
|
||||||
|
calls.push('next');
|
||||||
|
},
|
||||||
|
mpvSendCommand: (command) => {
|
||||||
|
sentCommands.push(command);
|
||||||
|
},
|
||||||
|
isMpvConnected: () => true,
|
||||||
|
hasRuntimeOptionsManager: () => true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
return { options, calls, sentCommands, osd };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('handleMpvCommandFromIpc forwards regular mpv commands', () => {
|
||||||
|
const { options, sentCommands, osd } = createOptions();
|
||||||
|
handleMpvCommandFromIpc(['cycle', 'pause'], options);
|
||||||
|
assert.deepEqual(sentCommands, [['cycle', 'pause']]);
|
||||||
|
assert.deepEqual(osd, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleMpvCommandFromIpc emits osd for subtitle position keybinding proxies', () => {
|
||||||
|
const { options, sentCommands, osd } = createOptions();
|
||||||
|
handleMpvCommandFromIpc(['add', 'sub-pos', 1], options);
|
||||||
|
assert.deepEqual(sentCommands, [['add', 'sub-pos', 1]]);
|
||||||
|
assert.deepEqual(osd, ['Subtitle position: ${sub-pos}']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleMpvCommandFromIpc emits osd for primary subtitle track keybinding proxies', () => {
|
||||||
|
const { options, sentCommands, osd } = createOptions();
|
||||||
|
handleMpvCommandFromIpc(['cycle', 'sid'], options);
|
||||||
|
assert.deepEqual(sentCommands, [['cycle', 'sid']]);
|
||||||
|
assert.deepEqual(osd, ['Subtitle track: ${sid}']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleMpvCommandFromIpc emits osd for secondary subtitle track keybinding proxies', () => {
|
||||||
|
const { options, sentCommands, osd } = createOptions();
|
||||||
|
handleMpvCommandFromIpc(['set_property', 'secondary-sid', 'auto'], options);
|
||||||
|
assert.deepEqual(sentCommands, [['set_property', 'secondary-sid', 'auto']]);
|
||||||
|
assert.deepEqual(osd, ['Secondary subtitle track: ${secondary-sid}']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleMpvCommandFromIpc does not forward commands while disconnected', () => {
|
||||||
|
const { options, sentCommands, osd } = createOptions({
|
||||||
|
isMpvConnected: () => false,
|
||||||
|
});
|
||||||
|
handleMpvCommandFromIpc(['add', 'sub-pos', 1], options);
|
||||||
|
assert.deepEqual(sentCommands, []);
|
||||||
|
assert.deepEqual(osd, []);
|
||||||
|
});
|
||||||
@@ -24,6 +24,31 @@ export interface HandleMpvCommandFromIpcOptions {
|
|||||||
hasRuntimeOptionsManager: () => boolean;
|
hasRuntimeOptionsManager: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MPV_PROPERTY_COMMANDS = new Set([
|
||||||
|
'add',
|
||||||
|
'set',
|
||||||
|
'set_property',
|
||||||
|
'cycle',
|
||||||
|
'cycle-values',
|
||||||
|
'multiply',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function resolveProxyCommandOsd(command: (string | number)[]): string | null {
|
||||||
|
const operation = typeof command[0] === 'string' ? command[0] : '';
|
||||||
|
const property = typeof command[1] === 'string' ? command[1] : '';
|
||||||
|
if (!MPV_PROPERTY_COMMANDS.has(operation)) return null;
|
||||||
|
if (property === 'sub-pos') {
|
||||||
|
return 'Subtitle position: ${sub-pos}';
|
||||||
|
}
|
||||||
|
if (property === 'sid') {
|
||||||
|
return 'Subtitle track: ${sid}';
|
||||||
|
}
|
||||||
|
if (property === 'secondary-sid') {
|
||||||
|
return 'Secondary subtitle track: ${secondary-sid}';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function handleMpvCommandFromIpc(
|
export function handleMpvCommandFromIpc(
|
||||||
command: (string | number)[],
|
command: (string | number)[],
|
||||||
options: HandleMpvCommandFromIpcOptions,
|
options: HandleMpvCommandFromIpcOptions,
|
||||||
@@ -58,6 +83,10 @@ export function handleMpvCommandFromIpc(
|
|||||||
options.mpvPlayNextSubtitle();
|
options.mpvPlayNextSubtitle();
|
||||||
} else {
|
} else {
|
||||||
options.mpvSendCommand(command);
|
options.mpvSendCommand(command);
|
||||||
|
const osd = resolveProxyCommandOsd(command);
|
||||||
|
if (osd) {
|
||||||
|
options.showMpvOsd(osd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user