From a1846ba23defa0ccad198cf93a992e58dde69dc6 Mon Sep 17 00:00:00 2001 From: sudacode Date: Mon, 9 Feb 2026 20:11:53 -0800 Subject: [PATCH] refactor: extract jimaku helpers and overlay shortcut service --- src/core/services/overlay-shortcut-service.ts | 158 +++++++++ src/jimaku/utils.ts | 108 ++++++ src/main.ts | 323 +++--------------- 3 files changed, 319 insertions(+), 270 deletions(-) create mode 100644 src/core/services/overlay-shortcut-service.ts diff --git a/src/core/services/overlay-shortcut-service.ts b/src/core/services/overlay-shortcut-service.ts new file mode 100644 index 0000000..0ca937c --- /dev/null +++ b/src/core/services/overlay-shortcut-service.ts @@ -0,0 +1,158 @@ +import { globalShortcut } from "electron"; +import { ConfiguredShortcuts } from "../utils/shortcut-config"; +import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback-service"; + +export interface OverlayShortcutHandlers { + copySubtitle: () => void; + copySubtitleMultiple: (timeoutMs: number) => void; + updateLastCardFromClipboard: () => void; + triggerFieldGrouping: () => void; + triggerSubsync: () => void; + mineSentence: () => void; + mineSentenceMultiple: (timeoutMs: number) => void; + toggleSecondarySub: () => void; + markAudioCard: () => void; + openRuntimeOptions: () => void; +} + +export function registerOverlayShortcutsService( + shortcuts: ConfiguredShortcuts, + handlers: OverlayShortcutHandlers, +): boolean { + let registeredAny = false; + const registerOverlayShortcut = ( + accelerator: string, + handler: () => void, + label: string, + ): void => { + if (isGlobalShortcutRegisteredSafe(accelerator)) { + registeredAny = true; + return; + } + const ok = globalShortcut.register(accelerator, handler); + if (!ok) { + console.warn( + `Failed to register overlay shortcut ${label}: ${accelerator}`, + ); + return; + } + registeredAny = true; + }; + + if (shortcuts.copySubtitleMultiple) { + registerOverlayShortcut( + shortcuts.copySubtitleMultiple, + () => handlers.copySubtitleMultiple(shortcuts.multiCopyTimeoutMs), + "copySubtitleMultiple", + ); + } + + if (shortcuts.copySubtitle) { + registerOverlayShortcut( + shortcuts.copySubtitle, + () => handlers.copySubtitle(), + "copySubtitle", + ); + } + + if (shortcuts.triggerFieldGrouping) { + registerOverlayShortcut( + shortcuts.triggerFieldGrouping, + () => handlers.triggerFieldGrouping(), + "triggerFieldGrouping", + ); + } + + if (shortcuts.triggerSubsync) { + registerOverlayShortcut( + shortcuts.triggerSubsync, + () => handlers.triggerSubsync(), + "triggerSubsync", + ); + } + + if (shortcuts.mineSentence) { + registerOverlayShortcut( + shortcuts.mineSentence, + () => handlers.mineSentence(), + "mineSentence", + ); + } + + if (shortcuts.mineSentenceMultiple) { + registerOverlayShortcut( + shortcuts.mineSentenceMultiple, + () => handlers.mineSentenceMultiple(shortcuts.multiCopyTimeoutMs), + "mineSentenceMultiple", + ); + } + + if (shortcuts.toggleSecondarySub) { + registerOverlayShortcut( + shortcuts.toggleSecondarySub, + () => handlers.toggleSecondarySub(), + "toggleSecondarySub", + ); + } + + if (shortcuts.updateLastCardFromClipboard) { + registerOverlayShortcut( + shortcuts.updateLastCardFromClipboard, + () => handlers.updateLastCardFromClipboard(), + "updateLastCardFromClipboard", + ); + } + + if (shortcuts.markAudioCard) { + registerOverlayShortcut( + shortcuts.markAudioCard, + () => handlers.markAudioCard(), + "markAudioCard", + ); + } + + if (shortcuts.openRuntimeOptions) { + registerOverlayShortcut( + shortcuts.openRuntimeOptions, + () => handlers.openRuntimeOptions(), + "openRuntimeOptions", + ); + } + + return registeredAny; +} + +export function unregisterOverlayShortcutsService( + shortcuts: ConfiguredShortcuts, +): void { + if (shortcuts.copySubtitle) { + globalShortcut.unregister(shortcuts.copySubtitle); + } + if (shortcuts.copySubtitleMultiple) { + globalShortcut.unregister(shortcuts.copySubtitleMultiple); + } + if (shortcuts.updateLastCardFromClipboard) { + globalShortcut.unregister(shortcuts.updateLastCardFromClipboard); + } + if (shortcuts.triggerFieldGrouping) { + globalShortcut.unregister(shortcuts.triggerFieldGrouping); + } + if (shortcuts.triggerSubsync) { + globalShortcut.unregister(shortcuts.triggerSubsync); + } + if (shortcuts.mineSentence) { + globalShortcut.unregister(shortcuts.mineSentence); + } + if (shortcuts.mineSentenceMultiple) { + globalShortcut.unregister(shortcuts.mineSentenceMultiple); + } + if (shortcuts.toggleSecondarySub) { + globalShortcut.unregister(shortcuts.toggleSecondarySub); + } + if (shortcuts.markAudioCard) { + globalShortcut.unregister(shortcuts.markAudioCard); + } + if (shortcuts.openRuntimeOptions) { + globalShortcut.unregister(shortcuts.openRuntimeOptions); + } +} diff --git a/src/jimaku/utils.ts b/src/jimaku/utils.ts index 48270a7..6175e55 100644 --- a/src/jimaku/utils.ts +++ b/src/jimaku/utils.ts @@ -1,10 +1,14 @@ import * as http from "http"; import * as https from "https"; import * as path from "path"; +import * as fs from "fs"; import * as childProcess from "child_process"; import { JimakuApiResponse, JimakuConfig, + JimakuDownloadResult, + JimakuFileEntry, + JimakuLanguagePreference, JimakuMediaInfo, } from "../types"; @@ -245,3 +249,107 @@ export function parseMediaInfo(mediaPath: string | null): JimakuMediaInfo { rawTitle: name, }; } + +function formatLangScore(name: string, pref: JimakuLanguagePreference): number { + if (pref === "none") return 0; + const upper = name.toUpperCase(); + const hasJa = + /(^|[\W_])JA([\W_]|$)/.test(upper) || + /(^|[\W_])JPN([\W_]|$)/.test(upper) || + upper.includes(".JA."); + const hasEn = + /(^|[\W_])EN([\W_]|$)/.test(upper) || + /(^|[\W_])ENG([\W_]|$)/.test(upper) || + upper.includes(".EN."); + if (pref === "ja") { + if (hasJa) return 2; + if (hasEn) return 1; + } else if (pref === "en") { + if (hasEn) return 2; + if (hasJa) return 1; + } + return 0; +} + +export function sortJimakuFiles( + files: JimakuFileEntry[], + pref: JimakuLanguagePreference, +): JimakuFileEntry[] { + if (pref === "none") return files; + return [...files].sort((a, b) => { + const scoreDiff = + formatLangScore(b.name, pref) - formatLangScore(a.name, pref); + if (scoreDiff !== 0) return scoreDiff; + return a.name.localeCompare(b.name); + }); +} + +export function isRemoteMediaPath(mediaPath: string): boolean { + return /^[a-z][a-z0-9+.-]*:\/\//i.test(mediaPath); +} + +export async function downloadToFile( + url: string, + destPath: string, + headers: Record, + redirectCount = 0, +): Promise { + if (redirectCount > 3) { + return { + ok: false, + error: { error: "Too many redirects while downloading subtitle." }, + }; + } + + return new Promise((resolve) => { + const parsedUrl = new URL(url); + const transport = parsedUrl.protocol === "https:" ? https : http; + + const req = transport.get(parsedUrl, { headers }, (res) => { + const status = res.statusCode || 0; + if ([301, 302, 303, 307, 308].includes(status) && res.headers.location) { + const redirectUrl = new URL(res.headers.location, parsedUrl).toString(); + res.resume(); + downloadToFile(redirectUrl, destPath, headers, redirectCount + 1).then( + resolve, + ); + return; + } + + if (status < 200 || status >= 300) { + res.resume(); + resolve({ + ok: false, + error: { + error: `Failed to download subtitle (HTTP ${status}).`, + code: status, + }, + }); + return; + } + + const fileStream = fs.createWriteStream(destPath); + res.pipe(fileStream); + fileStream.on("finish", () => { + fileStream.close(() => { + resolve({ ok: true, path: destPath }); + }); + }); + fileStream.on("error", (err: Error) => { + resolve({ + ok: false, + error: { + error: `Failed to save subtitle: ${(err as Error).message}`, + }, + }); + }); + }); + + req.on("error", (err) => { + resolve({ + ok: false, + error: { error: `Download request failed: ${(err as Error).message}` }, + }); + }); + }); +} diff --git a/src/main.ts b/src/main.ts index 2a78b31..114a3b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -91,9 +91,12 @@ import { SubtitleTimingTracker } from "./subtitle-timing-tracker"; import { AnkiIntegration } from "./anki-integration"; import { RuntimeOptionsManager } from "./runtime-options"; import { + downloadToFile, + isRemoteMediaPath, jimakuFetchJson as jimakuFetchJsonRequest, parseMediaInfo, resolveJimakuApiKey as resolveJimakuApiKeyFromConfig, + sortJimakuFiles, } from "./jimaku/utils"; import { CommandResult, @@ -136,6 +139,10 @@ import { isGlobalShortcutRegisteredSafe, shortcutMatchesInputForLocalFallback, } from "./core/services/shortcut-fallback-service"; +import { + registerOverlayShortcutsService, + unregisterOverlayShortcutsService, +} from "./core/services/overlay-shortcut-service"; import { ConfigService, DEFAULT_CONFIG, @@ -2841,110 +2848,6 @@ async function triggerSubsyncFromConfig(): Promise { } } -function formatLangScore(name: string, pref: JimakuLanguagePreference): number { - if (pref === "none") return 0; - const upper = name.toUpperCase(); - const hasJa = - /(^|[\W_])JA([\W_]|$)/.test(upper) || - /(^|[\W_])JPN([\W_]|$)/.test(upper) || - upper.includes(".JA."); - const hasEn = - /(^|[\W_])EN([\W_]|$)/.test(upper) || - /(^|[\W_])ENG([\W_]|$)/.test(upper) || - upper.includes(".EN."); - if (pref === "ja") { - if (hasJa) return 2; - if (hasEn) return 1; - } else if (pref === "en") { - if (hasEn) return 2; - if (hasJa) return 1; - } - return 0; -} - -function sortJimakuFiles( - files: JimakuFileEntry[], - pref: JimakuLanguagePreference, -): JimakuFileEntry[] { - if (pref === "none") return files; - return [...files].sort((a, b) => { - const scoreDiff = - formatLangScore(b.name, pref) - formatLangScore(a.name, pref); - if (scoreDiff !== 0) return scoreDiff; - return a.name.localeCompare(b.name); - }); -} - -function isRemoteMediaPath(mediaPath: string): boolean { - return /^[a-z][a-z0-9+.-]*:\/\//i.test(mediaPath); -} - -async function downloadToFile( - url: string, - destPath: string, - headers: Record, - redirectCount = 0, -): Promise { - if (redirectCount > 3) { - return { - ok: false, - error: { error: "Too many redirects while downloading subtitle." }, - }; - } - - return new Promise((resolve) => { - const parsedUrl = new URL(url); - const transport = parsedUrl.protocol === "https:" ? https : http; - - const req = transport.get(parsedUrl, { headers }, (res) => { - const status = res.statusCode || 0; - if ([301, 302, 303, 307, 308].includes(status) && res.headers.location) { - const redirectUrl = new URL(res.headers.location, parsedUrl).toString(); - res.resume(); - downloadToFile(redirectUrl, destPath, headers, redirectCount + 1).then( - resolve, - ); - return; - } - - if (status < 200 || status >= 300) { - res.resume(); - resolve({ - ok: false, - error: { - error: `Failed to download subtitle (HTTP ${status}).`, - code: status, - }, - }); - return; - } - - const fileStream = fs.createWriteStream(destPath); - res.pipe(fileStream); - fileStream.on("finish", () => { - fileStream.close(() => { - resolve({ ok: true, path: destPath }); - }); - }); - fileStream.on("error", (err) => { - resolve({ - ok: false, - error: { - error: `Failed to save subtitle: ${(err as Error).message}`, - }, - }); - }); - }); - - req.on("error", (err) => { - resolve({ - ok: false, - error: { error: `Download request failed: ${(err as Error).message}` }, - }); - }); - }); -} - function cancelPendingMultiCopy(): void { if (!pendingMultiCopy) return; @@ -3194,140 +3097,51 @@ function handleMineSentenceDigit(count: number): void { function registerOverlayShortcuts(): void { const shortcuts = getConfiguredShortcuts(); - let registeredAny = false; - const registerOverlayShortcut = ( - accelerator: string, - handler: () => void, - label: string, - ): void => { - if (isGlobalShortcutRegisteredSafe(accelerator)) { - registeredAny = true; - return; - } - const ok = globalShortcut.register(accelerator, handler); - if (!ok) { - console.warn( - `Failed to register overlay shortcut ${label}: ${accelerator}`, - ); - return; - } - registeredAny = true; - }; - - if (shortcuts.copySubtitleMultiple) { - registerOverlayShortcut( - shortcuts.copySubtitleMultiple, - () => { - startPendingMultiCopy(shortcuts.multiCopyTimeoutMs); - }, - "copySubtitleMultiple", - ); - } - - if (shortcuts.copySubtitle) { - registerOverlayShortcut( - shortcuts.copySubtitle, - () => { - copyCurrentSubtitle(); - }, - "copySubtitle", - ); - } - - if (shortcuts.triggerFieldGrouping) { - registerOverlayShortcut( - shortcuts.triggerFieldGrouping, - () => { - triggerFieldGrouping().catch((err) => { - console.error("triggerFieldGrouping failed:", err); - showMpvOsd(`Field grouping failed: ${(err as Error).message}`); - }); - }, - "triggerFieldGrouping", - ); - } - - if (shortcuts.triggerSubsync) { - registerOverlayShortcut( - shortcuts.triggerSubsync, - () => { - triggerSubsyncFromConfig().catch((err) => { - console.error("triggerSubsyncFromConfig failed:", err); - showMpvOsd(`Subsync failed: ${(err as Error).message}`); - }); - }, - "triggerSubsync", - ); - } - - if (shortcuts.mineSentence) { - registerOverlayShortcut( - shortcuts.mineSentence, - () => { - mineSentenceCard().catch((err) => { - console.error("mineSentenceCard failed:", err); - showMpvOsd(`Mine sentence failed: ${(err as Error).message}`); - }); - }, - "mineSentence", - ); - } - - if (shortcuts.mineSentenceMultiple) { - registerOverlayShortcut( - shortcuts.mineSentenceMultiple, - () => { - startPendingMineSentenceMultiple(shortcuts.multiCopyTimeoutMs); - }, - "mineSentenceMultiple", - ); - } - - if (shortcuts.toggleSecondarySub) { - registerOverlayShortcut( - shortcuts.toggleSecondarySub, - () => cycleSecondarySubMode(), - "toggleSecondarySub", - ); - } - - if (shortcuts.updateLastCardFromClipboard) { - registerOverlayShortcut( - shortcuts.updateLastCardFromClipboard, - () => { - updateLastCardFromClipboard().catch((err) => { - console.error("updateLastCardFromClipboard failed:", err); - showMpvOsd(`Update failed: ${(err as Error).message}`); - }); - }, - "updateLastCardFromClipboard", - ); - } - - if (shortcuts.markAudioCard) { - registerOverlayShortcut( - shortcuts.markAudioCard, - () => { - markLastCardAsAudioCard().catch((err) => { - console.error("markLastCardAsAudioCard failed:", err); - showMpvOsd(`Audio card failed: ${(err as Error).message}`); - }); - }, - "markAudioCard", - ); - } - - if (shortcuts.openRuntimeOptions) { - registerOverlayShortcut( - shortcuts.openRuntimeOptions, - () => { - openRuntimeOptionsPalette(); - }, - "openRuntimeOptions", - ); - } - - shortcutsRegistered = registeredAny; + shortcutsRegistered = registerOverlayShortcutsService(shortcuts, { + copySubtitle: () => { + copyCurrentSubtitle(); + }, + copySubtitleMultiple: (timeoutMs) => { + startPendingMultiCopy(timeoutMs); + }, + updateLastCardFromClipboard: () => { + updateLastCardFromClipboard().catch((err) => { + console.error("updateLastCardFromClipboard failed:", err); + showMpvOsd(`Update failed: ${(err as Error).message}`); + }); + }, + triggerFieldGrouping: () => { + triggerFieldGrouping().catch((err) => { + console.error("triggerFieldGrouping failed:", err); + showMpvOsd(`Field grouping failed: ${(err as Error).message}`); + }); + }, + triggerSubsync: () => { + triggerSubsyncFromConfig().catch((err) => { + console.error("triggerSubsyncFromConfig failed:", err); + showMpvOsd(`Subsync failed: ${(err as Error).message}`); + }); + }, + mineSentence: () => { + mineSentenceCard().catch((err) => { + console.error("mineSentenceCard failed:", err); + showMpvOsd(`Mine sentence failed: ${(err as Error).message}`); + }); + }, + mineSentenceMultiple: (timeoutMs) => { + startPendingMineSentenceMultiple(timeoutMs); + }, + toggleSecondarySub: () => cycleSecondarySubMode(), + markAudioCard: () => { + markLastCardAsAudioCard().catch((err) => { + console.error("markLastCardAsAudioCard failed:", err); + showMpvOsd(`Audio card failed: ${(err as Error).message}`); + }); + }, + openRuntimeOptions: () => { + openRuntimeOptionsPalette(); + }, + }); } function unregisterOverlayShortcuts(): void { @@ -3336,38 +3150,7 @@ function unregisterOverlayShortcuts(): void { cancelPendingMultiCopy(); cancelPendingMineSentenceMultiple(); - const shortcuts = getConfiguredShortcuts(); - - if (shortcuts.copySubtitle) { - globalShortcut.unregister(shortcuts.copySubtitle); - } - if (shortcuts.copySubtitleMultiple) { - globalShortcut.unregister(shortcuts.copySubtitleMultiple); - } - if (shortcuts.updateLastCardFromClipboard) { - globalShortcut.unregister(shortcuts.updateLastCardFromClipboard); - } - if (shortcuts.triggerFieldGrouping) { - globalShortcut.unregister(shortcuts.triggerFieldGrouping); - } - if (shortcuts.triggerSubsync) { - globalShortcut.unregister(shortcuts.triggerSubsync); - } - if (shortcuts.mineSentence) { - globalShortcut.unregister(shortcuts.mineSentence); - } - if (shortcuts.mineSentenceMultiple) { - globalShortcut.unregister(shortcuts.mineSentenceMultiple); - } - if (shortcuts.toggleSecondarySub) { - globalShortcut.unregister(shortcuts.toggleSecondarySub); - } - if (shortcuts.markAudioCard) { - globalShortcut.unregister(shortcuts.markAudioCard); - } - if (shortcuts.openRuntimeOptions) { - globalShortcut.unregister(shortcuts.openRuntimeOptions); - } + unregisterOverlayShortcutsService(getConfiguredShortcuts()); shortcutsRegistered = false; }