refactor: extract reusable numeric shortcut session runtime

This commit is contained in:
2026-02-09 22:29:50 -08:00
parent e773db7e88
commit 2ff66f1621
2 changed files with 140 additions and 111 deletions

View File

@@ -0,0 +1,99 @@
export interface NumericShortcutSessionMessages {
prompt: string;
timeout: string;
cancelled?: string;
}
export interface NumericShortcutSessionDeps {
registerShortcut: (accelerator: string, handler: () => void) => boolean;
unregisterShortcut: (accelerator: string) => void;
setTimer: (handler: () => void, timeoutMs: number) => ReturnType<typeof setTimeout>;
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
showMpvOsd: (text: string) => void;
}
export interface NumericShortcutSessionStartParams {
timeoutMs: number;
onDigit: (digit: number) => void;
messages: NumericShortcutSessionMessages;
}
export function createNumericShortcutSessionService(
deps: NumericShortcutSessionDeps,
) {
let active = false;
let timeout: ReturnType<typeof setTimeout> | null = null;
let digitShortcuts: string[] = [];
let escapeShortcut: string | null = null;
let cancelledMessage = "Cancelled";
const cancel = (showCancelled = false): void => {
if (!active) return;
active = false;
if (timeout) {
deps.clearTimer(timeout);
timeout = null;
}
for (const shortcut of digitShortcuts) {
deps.unregisterShortcut(shortcut);
}
digitShortcuts = [];
if (escapeShortcut) {
deps.unregisterShortcut(escapeShortcut);
escapeShortcut = null;
}
if (showCancelled) {
deps.showMpvOsd(cancelledMessage);
}
};
const start = ({
timeoutMs,
onDigit,
messages,
}: NumericShortcutSessionStartParams): void => {
cancel();
cancelledMessage = messages.cancelled ?? "Cancelled";
active = true;
for (let i = 1; i <= 9; i++) {
const shortcut = i.toString();
if (
deps.registerShortcut(shortcut, () => {
if (!active) return;
cancel();
onDigit(i);
})
) {
digitShortcuts.push(shortcut);
}
}
if (
deps.registerShortcut("Escape", () => {
cancel(true);
})
) {
escapeShortcut = "Escape";
}
timeout = deps.setTimer(() => {
if (!active) return;
cancel();
deps.showMpvOsd(messages.timeout);
}, timeoutMs);
deps.showMpvOsd(messages.prompt);
};
return {
start,
cancel,
isActive: (): boolean => active,
};
}

View File

@@ -116,6 +116,7 @@ import {
} from "./core/services/overlay-shortcut-service"; } from "./core/services/overlay-shortcut-service";
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner"; import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner";
import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-shortcut-runtime-service"; import { createOverlayShortcutRuntimeHandlers } from "./core/services/overlay-shortcut-runtime-service";
import { createNumericShortcutSessionService } from "./core/services/numeric-shortcut-session-service";
import { showDesktopNotification } from "./core/utils/notification"; import { showDesktopNotification } from "./core/utils/notification";
import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service"; import { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
import { tokenizeSubtitleService } from "./core/services/tokenizer-service"; import { tokenizeSubtitleService } from "./core/services/tokenizer-service";
@@ -250,16 +251,7 @@ let mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics = {
}; };
let shortcutsRegistered = false; let shortcutsRegistered = false;
let pendingMultiCopy = false;
let pendingMultiCopyTimeout: ReturnType<typeof setTimeout> | null = null;
let multiCopyDigitShortcuts: string[] = [];
let multiCopyEscapeShortcut: string | null = null;
let pendingMineSentenceMultiple = false;
let pendingMineSentenceMultipleTimeout: ReturnType<typeof setTimeout> | null =
null;
let overlayRuntimeInitialized = false; let overlayRuntimeInitialized = false;
let mineSentenceDigitShortcuts: string[] = [];
let mineSentenceEscapeShortcut: string | null = null;
let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null = let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
null; null;
let runtimeOptionsManager: RuntimeOptionsManager | null = null; let runtimeOptionsManager: RuntimeOptionsManager | null = null;
@@ -964,6 +956,24 @@ function showMpvOsd(text: string): void {
} }
} }
const multiCopySession = createNumericShortcutSessionService({
registerShortcut: (accelerator, handler) =>
globalShortcut.register(accelerator, handler),
unregisterShortcut: (accelerator) => globalShortcut.unregister(accelerator),
setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs),
clearTimer: (timer) => clearTimeout(timer),
showMpvOsd: (text) => showMpvOsd(text),
});
const mineSentenceSession = createNumericShortcutSessionService({
registerShortcut: (accelerator, handler) =>
globalShortcut.register(accelerator, handler),
unregisterShortcut: (accelerator) => globalShortcut.unregister(accelerator),
setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs),
clearTimer: (timer) => clearTimeout(timer),
showMpvOsd: (text) => showMpvOsd(text),
});
function getSubsyncServiceDeps() { function getSubsyncServiceDeps() {
return { return {
getMpvClient: () => mpvClient, getMpvClient: () => mpvClient,
@@ -988,61 +998,23 @@ async function triggerSubsyncFromConfig(): Promise<void> {
} }
function cancelPendingMultiCopy(): void { function cancelPendingMultiCopy(): void {
if (!pendingMultiCopy) return; multiCopySession.cancel();
pendingMultiCopy = false;
if (pendingMultiCopyTimeout) {
clearTimeout(pendingMultiCopyTimeout);
pendingMultiCopyTimeout = null;
}
for (const shortcut of multiCopyDigitShortcuts) {
globalShortcut.unregister(shortcut);
}
multiCopyDigitShortcuts = [];
if (multiCopyEscapeShortcut) {
globalShortcut.unregister(multiCopyEscapeShortcut);
multiCopyEscapeShortcut = null;
}
} }
function startPendingMultiCopy(timeoutMs: number): void { function startPendingMultiCopy(timeoutMs: number): void {
cancelPendingMultiCopy(); multiCopySession.start({
pendingMultiCopy = true; timeoutMs,
onDigit: (count) => handleMultiCopyDigit(count),
for (let i = 1; i <= 9; i++) { messages: {
const shortcut = i.toString(); prompt: "Copy how many lines? Press 1-9 (Esc to cancel)",
if ( timeout: "Copy timeout",
globalShortcut.register(shortcut, () => { cancelled: "Cancelled",
handleMultiCopyDigit(i); },
}) });
) {
multiCopyDigitShortcuts.push(shortcut);
}
}
if (
globalShortcut.register("Escape", () => {
cancelPendingMultiCopy();
showMpvOsd("Cancelled");
})
) {
multiCopyEscapeShortcut = "Escape";
}
pendingMultiCopyTimeout = setTimeout(() => {
cancelPendingMultiCopy();
showMpvOsd("Copy timeout");
}, timeoutMs);
showMpvOsd("Copy how many lines? Press 1-9 (Esc to cancel)");
} }
function handleMultiCopyDigit(count: number): void { function handleMultiCopyDigit(count: number): void {
if (!pendingMultiCopy || !subtitleTimingTracker) return; if (!subtitleTimingTracker) return;
cancelPendingMultiCopy();
const availableCount = Math.min(count, 200); // Max history size const availableCount = Math.min(count, 200); // Max history size
const blocks = subtitleTimingTracker.getRecentBlocks(availableCount); const blocks = subtitleTimingTracker.getRecentBlocks(availableCount);
@@ -1135,67 +1107,25 @@ async function mineSentenceCard(): Promise<void> {
} }
function cancelPendingMineSentenceMultiple(): void { function cancelPendingMineSentenceMultiple(): void {
if (!pendingMineSentenceMultiple) return; mineSentenceSession.cancel();
pendingMineSentenceMultiple = false;
if (pendingMineSentenceMultipleTimeout) {
clearTimeout(pendingMineSentenceMultipleTimeout);
pendingMineSentenceMultipleTimeout = null;
}
for (const shortcut of mineSentenceDigitShortcuts) {
globalShortcut.unregister(shortcut);
}
mineSentenceDigitShortcuts = [];
if (mineSentenceEscapeShortcut) {
globalShortcut.unregister(mineSentenceEscapeShortcut);
mineSentenceEscapeShortcut = null;
}
} }
function startPendingMineSentenceMultiple(timeoutMs: number): void { function startPendingMineSentenceMultiple(timeoutMs: number): void {
cancelPendingMineSentenceMultiple(); mineSentenceSession.start({
pendingMineSentenceMultiple = true; timeoutMs,
onDigit: (count) => handleMineSentenceDigit(count),
for (let i = 1; i <= 9; i++) { messages: {
const shortcut = i.toString(); prompt: "Mine how many lines? Press 1-9 (Esc to cancel)",
if ( timeout: "Mine sentence timeout",
globalShortcut.register(shortcut, () => { cancelled: "Cancelled",
handleMineSentenceDigit(i); },
}) });
) {
mineSentenceDigitShortcuts.push(shortcut);
}
}
if (
globalShortcut.register("Escape", () => {
cancelPendingMineSentenceMultiple();
showMpvOsd("Cancelled");
})
) {
mineSentenceEscapeShortcut = "Escape";
}
pendingMineSentenceMultipleTimeout = setTimeout(() => {
cancelPendingMineSentenceMultiple();
showMpvOsd("Mine sentence timeout");
}, timeoutMs);
showMpvOsd("Mine how many lines? Press 1-9 (Esc to cancel)");
} }
function handleMineSentenceDigit(count: number): void { function handleMineSentenceDigit(count: number): void {
if ( if (!subtitleTimingTracker || !ankiIntegration)
!pendingMineSentenceMultiple ||
!subtitleTimingTracker ||
!ankiIntegration
)
return; return;
cancelPendingMineSentenceMultiple();
const blocks = subtitleTimingTracker.getRecentBlocks(count); const blocks = subtitleTimingTracker.getRecentBlocks(count);
if (blocks.length === 0) { if (blocks.length === 0) {