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";
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-fallback-runner";
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 { openYomitanSettingsWindow } from "./core/services/yomitan-settings-service";
import { tokenizeSubtitleService } from "./core/services/tokenizer-service";
@@ -250,16 +251,7 @@ let mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics = {
};
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 mineSentenceDigitShortcuts: string[] = [];
let mineSentenceEscapeShortcut: string | null = null;
let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | 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() {
return {
getMpvClient: () => mpvClient,
@@ -988,61 +998,23 @@ async function triggerSubsyncFromConfig(): Promise<void> {
}
function cancelPendingMultiCopy(): void {
if (!pendingMultiCopy) return;
pendingMultiCopy = false;
if (pendingMultiCopyTimeout) {
clearTimeout(pendingMultiCopyTimeout);
pendingMultiCopyTimeout = null;
}
for (const shortcut of multiCopyDigitShortcuts) {
globalShortcut.unregister(shortcut);
}
multiCopyDigitShortcuts = [];
if (multiCopyEscapeShortcut) {
globalShortcut.unregister(multiCopyEscapeShortcut);
multiCopyEscapeShortcut = null;
}
multiCopySession.cancel();
}
function startPendingMultiCopy(timeoutMs: number): void {
cancelPendingMultiCopy();
pendingMultiCopy = true;
for (let i = 1; i <= 9; i++) {
const shortcut = i.toString();
if (
globalShortcut.register(shortcut, () => {
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)");
multiCopySession.start({
timeoutMs,
onDigit: (count) => handleMultiCopyDigit(count),
messages: {
prompt: "Copy how many lines? Press 1-9 (Esc to cancel)",
timeout: "Copy timeout",
cancelled: "Cancelled",
},
});
}
function handleMultiCopyDigit(count: number): void {
if (!pendingMultiCopy || !subtitleTimingTracker) return;
cancelPendingMultiCopy();
if (!subtitleTimingTracker) return;
const availableCount = Math.min(count, 200); // Max history size
const blocks = subtitleTimingTracker.getRecentBlocks(availableCount);
@@ -1135,67 +1107,25 @@ async function mineSentenceCard(): Promise<void> {
}
function cancelPendingMineSentenceMultiple(): void {
if (!pendingMineSentenceMultiple) return;
pendingMineSentenceMultiple = false;
if (pendingMineSentenceMultipleTimeout) {
clearTimeout(pendingMineSentenceMultipleTimeout);
pendingMineSentenceMultipleTimeout = null;
}
for (const shortcut of mineSentenceDigitShortcuts) {
globalShortcut.unregister(shortcut);
}
mineSentenceDigitShortcuts = [];
if (mineSentenceEscapeShortcut) {
globalShortcut.unregister(mineSentenceEscapeShortcut);
mineSentenceEscapeShortcut = null;
}
mineSentenceSession.cancel();
}
function startPendingMineSentenceMultiple(timeoutMs: number): void {
cancelPendingMineSentenceMultiple();
pendingMineSentenceMultiple = true;
for (let i = 1; i <= 9; i++) {
const shortcut = i.toString();
if (
globalShortcut.register(shortcut, () => {
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)");
mineSentenceSession.start({
timeoutMs,
onDigit: (count) => handleMineSentenceDigit(count),
messages: {
prompt: "Mine how many lines? Press 1-9 (Esc to cancel)",
timeout: "Mine sentence timeout",
cancelled: "Cancelled",
},
});
}
function handleMineSentenceDigit(count: number): void {
if (
!pendingMineSentenceMultiple ||
!subtitleTimingTracker ||
!ankiIntegration
)
if (!subtitleTimingTracker || !ankiIntegration)
return;
cancelPendingMineSentenceMultiple();
const blocks = subtitleTimingTracker.getRecentBlocks(count);
if (blocks.length === 0) {