mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-24 05:16:19 -07:00
feat(overlay): add optional subtitle selection modal (#265)
This commit is contained in:
@@ -93,7 +93,7 @@ export interface IpcServiceDeps {
|
||||
handleMpvCommand: (command: Array<string | number>) => void;
|
||||
getKeybindings: () => unknown;
|
||||
getMpvInputBindings?: () => Promise<MpvInputBindingsSnapshot>;
|
||||
getSessionBindings?: () => CompiledSessionBinding[];
|
||||
getSessionBindings?: () => CompiledSessionBinding[] | Promise<CompiledSessionBinding[]>;
|
||||
getConfiguredShortcuts: () => unknown;
|
||||
dispatchSessionAction?: (request: SessionActionDispatchRequest) => void | Promise<void>;
|
||||
getStatsToggleKey: () => string;
|
||||
@@ -378,7 +378,7 @@ export interface IpcDepsRuntimeOptions {
|
||||
handleMpvCommand: (command: Array<string | number>) => void;
|
||||
getKeybindings: () => unknown;
|
||||
getMpvInputBindings?: () => Promise<MpvInputBindingsSnapshot>;
|
||||
getSessionBindings?: () => CompiledSessionBinding[];
|
||||
getSessionBindings?: () => CompiledSessionBinding[] | Promise<CompiledSessionBinding[]>;
|
||||
getConfiguredShortcuts: () => unknown;
|
||||
dispatchSessionAction?: (request: SessionActionDispatchRequest) => void | Promise<void>;
|
||||
getStatsToggleKey: () => string;
|
||||
|
||||
@@ -29,6 +29,7 @@ function makeShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configured
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openTsukihime: null,
|
||||
openSubtitleSelection: null,
|
||||
openSubtitleGeneration: null,
|
||||
openSessionHelp: null,
|
||||
openControllerSelect: null,
|
||||
|
||||
@@ -24,6 +24,7 @@ function createShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configur
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openTsukihime: null,
|
||||
openSubtitleSelection: null,
|
||||
openSubtitleGeneration: null,
|
||||
openSessionHelp: null,
|
||||
openControllerSelect: null,
|
||||
|
||||
@@ -43,6 +43,7 @@ function createDeps(overrides: Partial<SessionActionExecutorDeps> = {}) {
|
||||
openControllerDebug: () => calls.push('controller-debug'),
|
||||
openJimaku: () => calls.push('jimaku'),
|
||||
openTsukihime: () => calls.push('tsukihime'),
|
||||
openSubtitleSelection: () => calls.push('subtitle-selection'),
|
||||
openSubtitleGeneration: () => calls.push('subtitle-generation'),
|
||||
openYoutubeTrackPicker: () => {
|
||||
calls.push('youtube');
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface SessionActionExecutorDeps {
|
||||
openControllerDebug: () => void;
|
||||
openJimaku: () => void;
|
||||
openTsukihime: () => void;
|
||||
openSubtitleSelection: () => void;
|
||||
openSubtitleGeneration: () => void;
|
||||
openYoutubeTrackPicker: () => void | Promise<void>;
|
||||
openPlaylistBrowser: () => boolean | void | Promise<boolean | void>;
|
||||
@@ -120,6 +121,9 @@ export async function dispatchSessionAction(
|
||||
case 'openTsukihime':
|
||||
deps.openTsukihime();
|
||||
return;
|
||||
case 'openSubtitleSelection':
|
||||
deps.openSubtitleSelection();
|
||||
return;
|
||||
case 'openSubtitleGeneration':
|
||||
deps.openSubtitleGeneration();
|
||||
return;
|
||||
|
||||
@@ -24,6 +24,7 @@ function createShortcuts(overrides: Partial<ConfiguredShortcuts> = {}): Configur
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openTsukihime: null,
|
||||
openSubtitleSelection: null,
|
||||
openSubtitleGeneration: null,
|
||||
openSessionHelp: null,
|
||||
openControllerSelect: null,
|
||||
@@ -707,3 +708,58 @@ test('buildPluginSessionBindingsArtifact preserves plugin selector CLI for no-co
|
||||
assert.equal(byActionId.get('copySubtitleMultiple')?.cliArgs, undefined);
|
||||
assert.equal(byActionId.get('mineSentenceMultiple')?.cliArgs, undefined);
|
||||
});
|
||||
|
||||
test('single keys reserve sequence prefixes without reserving the second stroke', () => {
|
||||
for (const key of ['g', 'Ctrl+g', 's']) {
|
||||
const result = compileSessionBindings({
|
||||
shortcuts: createShortcuts({ openSubtitleSelection: 'g-s' }),
|
||||
keybindings: [createKeybinding(key, ['show-text', 'single'])],
|
||||
platform: 'linux',
|
||||
});
|
||||
assert.ok(result.bindings.some((binding) => binding.originalKey === key));
|
||||
assert.equal(
|
||||
result.bindings.some((binding) => binding.originalKey === 'g-s'),
|
||||
key !== 'g',
|
||||
);
|
||||
assert.equal(result.warnings.length, key === 'g' ? 1 : 0);
|
||||
if (key === 'g') assert.match(result.warnings[0]!.message, /Single-key bindings take priority/);
|
||||
}
|
||||
});
|
||||
|
||||
test('configured shortcuts and built-in overlay keys also reserve sequence prefixes', () => {
|
||||
for (const prefix of ['g', 'y', 'v']) {
|
||||
const result = compileSessionBindings({
|
||||
shortcuts: createShortcuts({ openSubtitleSelection: `${prefix}-s`, copySubtitle: 'g' }),
|
||||
keybindings: [],
|
||||
platform: 'linux',
|
||||
});
|
||||
assert.equal(
|
||||
result.bindings.some((binding) => binding.originalKey === `${prefix}-s`),
|
||||
false,
|
||||
);
|
||||
assert.ok(result.bindings.some((binding) => binding.originalKey === 'g'));
|
||||
assert.equal(result.warnings.length, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('sequence reservations follow the sidebar code and literal Shift semantics', () => {
|
||||
for (const [toggleKey, disabled] of [
|
||||
['KeyG', true],
|
||||
['g', false],
|
||||
['G', true],
|
||||
] as const) {
|
||||
const result = compileSessionBindings({
|
||||
shortcuts: createShortcuts({ openSubtitleSelection: 'Shift+g-s' }),
|
||||
keybindings: [],
|
||||
platform: 'linux',
|
||||
rawConfig: {
|
||||
...DEFAULT_CONFIG,
|
||||
subtitleSidebar: { ...DEFAULT_CONFIG.subtitleSidebar, toggleKey },
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
result.bindings.some((binding) => binding.originalKey === 'Shift+g-s'),
|
||||
!disabled,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,6 +12,10 @@ import type {
|
||||
SessionKeySpec,
|
||||
} from '../../types/session-bindings';
|
||||
import { SPECIAL_COMMANDS } from '../../config';
|
||||
import {
|
||||
resolveSessionSequenceConflicts,
|
||||
type SessionKeyReservation,
|
||||
} from '../../shared/session-key-sequences';
|
||||
|
||||
type PlatformKeyModel = 'darwin' | 'win32' | 'linux';
|
||||
|
||||
@@ -56,6 +60,7 @@ const SESSION_SHORTCUT_ACTIONS: Array<{
|
||||
{ key: 'openRuntimeOptions', actionId: 'openRuntimeOptions' },
|
||||
{ key: 'openJimaku', actionId: 'openJimaku' },
|
||||
{ key: 'openTsukihime', actionId: 'openTsukihime' },
|
||||
{ key: 'openSubtitleSelection', actionId: 'openSubtitleSelection' },
|
||||
{ key: 'openSubtitleGeneration', actionId: 'openSubtitleGeneration' },
|
||||
{ key: 'openSessionHelp', actionId: 'openSessionHelp' },
|
||||
{ key: 'openControllerSelect', actionId: 'openControllerSelect' },
|
||||
@@ -81,6 +86,13 @@ function normalizeCodeToken(
|
||||
): string | null {
|
||||
const normalized = token.trim();
|
||||
if (!normalized) return null;
|
||||
// Two lowercase letters use mpv's sequential-key syntax, for example g-s.
|
||||
if (/^[a-z]-[a-z]$/.test(normalized)) {
|
||||
return normalized
|
||||
.split('-')
|
||||
.map((letter) => `Key${letter.toUpperCase()}`)
|
||||
.join('-');
|
||||
}
|
||||
if (options.allowMouseButtons === true) {
|
||||
const normalizedMouse = normalized.toUpperCase();
|
||||
if (MPV_MOUSE_BUTTON_CODES.has(normalizedMouse)) {
|
||||
@@ -543,7 +555,37 @@ export function compileSessionBindings(input: CompileSessionBindingsInput): {
|
||||
}
|
||||
|
||||
bindings.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
||||
return { bindings, warnings };
|
||||
const reservations: SessionKeyReservation[] = [
|
||||
{ key: { code: 'KeyY', modifiers: [] }, path: 'built-in y sequences' },
|
||||
{ key: { code: 'KeyV', modifiers: [] }, path: 'primary subtitle visibility key' },
|
||||
{ key: { code: 'KeyY', modifiers: ['ctrl'] }, path: 'lookup window toggle' },
|
||||
{ key: { code: 'KeyY', modifiers: ['meta'] }, path: 'lookup window toggle' },
|
||||
{ key: { code: 'KeyY', modifiers: ['ctrl', 'shift'] }, path: 'keyboard-driven mode toggle' },
|
||||
{ key: { code: 'KeyY', modifiers: ['shift', 'meta'] }, path: 'keyboard-driven mode toggle' },
|
||||
...[...candidates.values()].flatMap((drafts) =>
|
||||
drafts.map(({ binding }) => ({
|
||||
key: binding.key,
|
||||
path: binding.sourcePath,
|
||||
})),
|
||||
),
|
||||
];
|
||||
const sidebarKey = input.rawConfig?.subtitleSidebar?.toggleKey;
|
||||
if (sidebarKey) {
|
||||
const { key } = parseSessionBindingKey(sidebarKey, input.platform);
|
||||
if (key) {
|
||||
// The sidebar accepts DOM codes with either Shift state, or literal characters.
|
||||
if (/^[A-Z]$/.test(sidebarKey)) key.modifiers = ['shift'];
|
||||
reservations.push({ key, path: 'subtitleSidebar.toggleKey' });
|
||||
if (/^Key[A-Z]$/.test(sidebarKey)) {
|
||||
reservations.push({
|
||||
key: { ...key, modifiers: ['shift'] },
|
||||
path: 'subtitleSidebar.toggleKey',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = resolveSessionSequenceConflicts(bindings, reservations);
|
||||
return { bindings: result.bindings, warnings: [...warnings, ...result.warnings] };
|
||||
}
|
||||
|
||||
export function buildPluginSessionBindingsArtifact(input: {
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface ConfiguredShortcuts {
|
||||
openRuntimeOptions: string | null | undefined;
|
||||
openJimaku: string | null | undefined;
|
||||
openTsukihime: string | null | undefined;
|
||||
openSubtitleSelection: string | null | undefined;
|
||||
openSubtitleGeneration: string | null | undefined;
|
||||
openSessionHelp: string | null | undefined;
|
||||
openControllerSelect: string | null | undefined;
|
||||
@@ -68,6 +69,10 @@ export function resolveConfiguredShortcuts(
|
||||
openRuntimeOptions: normalizeShortcut(shortcutValue('openRuntimeOptions')),
|
||||
openJimaku: normalizeShortcut(shortcutValue('openJimaku')),
|
||||
openTsukihime: normalizeShortcut(shortcutValue('openTsukihime')),
|
||||
openSubtitleSelection:
|
||||
config.subtitleSelection?.enabled === true
|
||||
? normalizeShortcut(shortcutValue('openSubtitleSelection'))
|
||||
: null,
|
||||
openSubtitleGeneration: normalizeShortcut(shortcutValue('openSubtitleGeneration')),
|
||||
openSessionHelp: normalizeShortcut(shortcutValue('openSessionHelp')),
|
||||
openControllerSelect: normalizeShortcut(shortcutValue('openControllerSelect')),
|
||||
|
||||
Reference in New Issue
Block a user