mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
0298a066ad
- Reorganize Configuration window into Appearance, Behavior, Anki, Input, and Integration sections - Add AnkiConnect-backed deck, note-type, and field pickers in the Anki section - Add click-to-learn keybinding controls - Move known-word and N+1 highlight colors to subtitleStyle.knownWordColor / subtitleStyle.nPlusOneColor; legacy ankiConnect.knownWords.color and ankiConnect.nPlusOne.nPlusOne keys still accepted with deprecation warnings - Add deckNames, modelNames, modelFieldNames, and fieldNamesForDeck methods to AnkiConnectClient - Mark discordPresence.presenceStyle as an enum in the config registry
218 lines
5.8 KiB
TypeScript
218 lines
5.8 KiB
TypeScript
import type { Keybinding } from '../types/runtime';
|
|
|
|
export type KeyInputMode = 'accelerator' | 'dom-code' | 'code';
|
|
|
|
export interface KeyboardInputLike {
|
|
code: string;
|
|
key: string;
|
|
ctrlKey: boolean;
|
|
altKey: boolean;
|
|
shiftKey: boolean;
|
|
metaKey: boolean;
|
|
}
|
|
|
|
export interface MpvKeybindingRow {
|
|
defaultKey: string;
|
|
key: string;
|
|
command: (string | number)[] | null;
|
|
commandText: string;
|
|
isDefault: boolean;
|
|
}
|
|
|
|
const MODIFIER_CODES = new Set([
|
|
'AltLeft',
|
|
'AltRight',
|
|
'ControlLeft',
|
|
'ControlRight',
|
|
'MetaLeft',
|
|
'MetaRight',
|
|
'ShiftLeft',
|
|
'ShiftRight',
|
|
]);
|
|
|
|
const ELECTRON_KEY_BY_CODE: Record<string, string> = {
|
|
Backquote: 'Backquote',
|
|
Backslash: 'Backslash',
|
|
BracketLeft: 'BracketLeft',
|
|
BracketRight: 'BracketRight',
|
|
Comma: 'Comma',
|
|
Delete: 'Delete',
|
|
End: 'End',
|
|
Enter: 'Enter',
|
|
Equal: 'Plus',
|
|
Escape: 'Escape',
|
|
Home: 'Home',
|
|
Insert: 'Insert',
|
|
Minus: 'Minus',
|
|
PageDown: 'PageDown',
|
|
PageUp: 'PageUp',
|
|
Period: 'Period',
|
|
Quote: 'Quote',
|
|
Semicolon: 'Semicolon',
|
|
Slash: 'Slash',
|
|
Space: 'Space',
|
|
Tab: 'Tab',
|
|
};
|
|
|
|
function commandEquals(a: Keybinding['command'], b: Keybinding['command']): boolean {
|
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
}
|
|
|
|
function normalizeUserBindings(userBindings: unknown): Keybinding[] {
|
|
if (!Array.isArray(userBindings)) return [];
|
|
return userBindings.filter((binding): binding is Keybinding => {
|
|
if (!binding || typeof binding !== 'object') return false;
|
|
const candidate = binding as Keybinding;
|
|
return (
|
|
typeof candidate.key === 'string' &&
|
|
(candidate.command === null || Array.isArray(candidate.command))
|
|
);
|
|
});
|
|
}
|
|
|
|
function electronKeyToken(input: KeyboardInputLike): string | null {
|
|
if (/^Key[A-Z]$/.test(input.code)) return input.code.slice(3);
|
|
if (/^Digit[0-9]$/.test(input.code)) return input.code.slice(5);
|
|
if (/^Numpad[0-9]$/.test(input.code)) return `num${input.code.slice(6)}`;
|
|
if (/^F\d{1,2}$/.test(input.code)) return input.code;
|
|
if (input.code.startsWith('Arrow')) return input.code.replace('Arrow', '');
|
|
return ELECTRON_KEY_BY_CODE[input.code] ?? null;
|
|
}
|
|
|
|
export function keyboardEventToConfigKey(
|
|
input: KeyboardInputLike,
|
|
mode: KeyInputMode,
|
|
): string | null {
|
|
if (!input.code || MODIFIER_CODES.has(input.code)) {
|
|
return null;
|
|
}
|
|
|
|
if (mode === 'code') {
|
|
return input.code;
|
|
}
|
|
|
|
const parts: string[] = [];
|
|
if (mode === 'accelerator') {
|
|
if (input.ctrlKey || input.metaKey) parts.push('CommandOrControl');
|
|
if (input.altKey) parts.push('Alt');
|
|
if (input.shiftKey) parts.push('Shift');
|
|
const key = electronKeyToken(input);
|
|
return key ? [...parts, key].join('+') : null;
|
|
}
|
|
|
|
if (input.ctrlKey) parts.push('Ctrl');
|
|
if (input.altKey) parts.push('Alt');
|
|
if (input.shiftKey) parts.push('Shift');
|
|
if (input.metaKey) parts.push('Meta');
|
|
return [...parts, input.code].join('+');
|
|
}
|
|
|
|
export function createMpvKeybindingRows(
|
|
defaultBindings: Keybinding[],
|
|
userBindings: unknown,
|
|
): MpvKeybindingRow[] {
|
|
const normalizedUserBindings = normalizeUserBindings(userBindings);
|
|
const userByKey = new Map(normalizedUserBindings.map((binding) => [binding.key, binding]));
|
|
const consumedUserKeys = new Set<string>();
|
|
|
|
const rows = defaultBindings.map((binding) => {
|
|
const override = userByKey.get(binding.key);
|
|
if (override?.command === null) {
|
|
const movedOverride = normalizedUserBindings.find(
|
|
(candidate) =>
|
|
candidate.key !== binding.key && commandEquals(candidate.command, binding.command),
|
|
);
|
|
if (movedOverride) {
|
|
consumedUserKeys.add(binding.key);
|
|
consumedUserKeys.add(movedOverride.key);
|
|
return {
|
|
defaultKey: binding.key,
|
|
key: movedOverride.key,
|
|
command: movedOverride.command,
|
|
commandText: JSON.stringify(movedOverride.command ?? null),
|
|
isDefault: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (override) {
|
|
consumedUserKeys.add(binding.key);
|
|
}
|
|
const command = override?.command ?? binding.command;
|
|
return {
|
|
defaultKey: binding.key,
|
|
key: binding.key,
|
|
command,
|
|
commandText: JSON.stringify(command ?? null),
|
|
isDefault: true,
|
|
};
|
|
});
|
|
|
|
for (const binding of normalizedUserBindings) {
|
|
if (consumedUserKeys.has(binding.key)) {
|
|
continue;
|
|
}
|
|
if (defaultBindings.some((defaultBinding) => defaultBinding.key === binding.key)) {
|
|
continue;
|
|
}
|
|
rows.push({
|
|
defaultKey: binding.key,
|
|
key: binding.key,
|
|
command: binding.command,
|
|
commandText: JSON.stringify(binding.command ?? null),
|
|
isDefault: false,
|
|
});
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
export function parseMpvCommandText(value: string): Keybinding['command'] | undefined {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
if (parsed === null) return null;
|
|
if (
|
|
Array.isArray(parsed) &&
|
|
parsed.every((entry) => typeof entry === 'string' || typeof entry === 'number')
|
|
) {
|
|
return parsed;
|
|
}
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function buildMpvKeybindingConfigValue(
|
|
defaultBindings: Keybinding[],
|
|
rows: MpvKeybindingRow[],
|
|
): Keybinding[] {
|
|
const next: Keybinding[] = [];
|
|
|
|
for (const defaultBinding of defaultBindings) {
|
|
const row = rows.find((candidate) => candidate.defaultKey === defaultBinding.key);
|
|
if (!row) {
|
|
next.push({ key: defaultBinding.key, command: null });
|
|
continue;
|
|
}
|
|
|
|
if (row.key !== defaultBinding.key) {
|
|
next.push({ key: defaultBinding.key, command: null });
|
|
if (row.command !== null) {
|
|
next.push({ key: row.key, command: row.command });
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!commandEquals(row.command, defaultBinding.command)) {
|
|
next.push({ key: row.key, command: row.command });
|
|
}
|
|
}
|
|
|
|
for (const row of rows.filter((candidate) => !candidate.isDefault)) {
|
|
next.push({ key: row.key, command: row.command });
|
|
}
|
|
|
|
return next;
|
|
}
|