mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract overlay shortcuts runtime for task 27.2
This commit is contained in:
@@ -20,7 +20,6 @@ import { AnkiConnectClient } from "./anki-connect";
|
|||||||
import { SubtitleTimingTracker } from "./subtitle-timing-tracker";
|
import { SubtitleTimingTracker } from "./subtitle-timing-tracker";
|
||||||
import { MediaGenerator } from "./media-generator";
|
import { MediaGenerator } from "./media-generator";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import axios from "axios";
|
|
||||||
import {
|
import {
|
||||||
AnkiConnectConfig,
|
AnkiConnectConfig,
|
||||||
KikuDuplicateCardInfo,
|
KikuDuplicateCardInfo,
|
||||||
@@ -31,6 +30,11 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
import { DEFAULT_ANKI_CONNECT_CONFIG } from "./config";
|
import { DEFAULT_ANKI_CONNECT_CONFIG } from "./config";
|
||||||
import { createLogger } from "./logger";
|
import { createLogger } from "./logger";
|
||||||
|
import {
|
||||||
|
AiTranslateCallbacks,
|
||||||
|
AiTranslateRequest,
|
||||||
|
translateSentenceWithAi,
|
||||||
|
} from "./anki-integration/ai";
|
||||||
|
|
||||||
const log = createLogger("anki").child("integration");
|
const log = createLogger("anki").child("integration");
|
||||||
|
|
||||||
@@ -134,93 +138,6 @@ export class AnkiIntegration {
|
|||||||
this.fieldGroupingCallback = fieldGroupingCallback || null;
|
this.fieldGroupingCallback = fieldGroupingCallback || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractAiText(content: unknown): string {
|
|
||||||
if (typeof content === "string") {
|
|
||||||
return content.trim();
|
|
||||||
}
|
|
||||||
if (!Array.isArray(content)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const parts: string[] = [];
|
|
||||||
for (const item of content) {
|
|
||||||
if (
|
|
||||||
item &&
|
|
||||||
typeof item === "object" &&
|
|
||||||
"type" in item &&
|
|
||||||
(item as { type?: unknown }).type === "text" &&
|
|
||||||
"text" in item &&
|
|
||||||
typeof (item as { text?: unknown }).text === "string"
|
|
||||||
) {
|
|
||||||
parts.push((item as { text: string }).text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parts.join("").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeOpenAiBaseUrl(baseUrl: string): string {
|
|
||||||
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
|
||||||
if (/\/v1$/i.test(trimmed)) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
return `${trimmed}/v1`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async translateSentenceWithAi(
|
|
||||||
sentence: string,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const ai = this.config.ai ?? DEFAULT_ANKI_CONNECT_CONFIG.ai;
|
|
||||||
if (!ai) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const apiKey = ai?.apiKey?.trim();
|
|
||||||
if (!apiKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl = this.normalizeOpenAiBaseUrl(
|
|
||||||
ai.baseUrl || "https://openrouter.ai/api",
|
|
||||||
);
|
|
||||||
const model = ai.model || "openai/gpt-4o-mini";
|
|
||||||
const targetLanguage = ai.targetLanguage || "English";
|
|
||||||
const defaultSystemPrompt =
|
|
||||||
"You are a translation engine. Return only the translated text with no explanations.";
|
|
||||||
const systemPrompt = ai.systemPrompt?.trim() || defaultSystemPrompt;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(
|
|
||||||
`${baseUrl}/chat/completions`,
|
|
||||||
{
|
|
||||||
model,
|
|
||||||
temperature: 0,
|
|
||||||
messages: [
|
|
||||||
{ role: "system", content: systemPrompt },
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `Translate this text to ${targetLanguage}:\n\n${sentence}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
timeout: 15000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const content = (response.data as { choices?: unknown[] })?.choices?.[0] as
|
|
||||||
| { message?: { content?: unknown } }
|
|
||||||
| undefined;
|
|
||||||
const translated = this.extractAiText(content?.message?.content);
|
|
||||||
return translated || null;
|
|
||||||
} catch (error) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error ? error.message : "Unknown translation error";
|
|
||||||
log.warn("AI translation failed:", message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getLapisConfig(): {
|
private getLapisConfig(): {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sentenceCardModel?: string;
|
sentenceCardModel?: string;
|
||||||
@@ -1528,7 +1445,18 @@ export class AnkiIntegration {
|
|||||||
const shouldAttemptAiTranslation =
|
const shouldAttemptAiTranslation =
|
||||||
aiEnabled && (alwaysUseAiTranslation || !hasSecondarySub);
|
aiEnabled && (alwaysUseAiTranslation || !hasSecondarySub);
|
||||||
if (shouldAttemptAiTranslation) {
|
if (shouldAttemptAiTranslation) {
|
||||||
const translated = await this.translateSentenceWithAi(sentence);
|
const request: AiTranslateRequest = {
|
||||||
|
sentence,
|
||||||
|
apiKey: aiConfig?.apiKey || "",
|
||||||
|
baseUrl: aiConfig?.baseUrl,
|
||||||
|
model: aiConfig?.model,
|
||||||
|
targetLanguage: aiConfig?.targetLanguage,
|
||||||
|
systemPrompt: aiConfig?.systemPrompt,
|
||||||
|
};
|
||||||
|
const callbacks: AiTranslateCallbacks = {
|
||||||
|
logWarning: (message: string) => log.warn(message),
|
||||||
|
};
|
||||||
|
const translated = await translateSentenceWithAi(request, callbacks);
|
||||||
if (translated) {
|
if (translated) {
|
||||||
backText = translated;
|
backText = translated;
|
||||||
} else if (!hasSecondarySub) {
|
} else if (!hasSecondarySub) {
|
||||||
|
|||||||
103
src/anki-integration/ai.ts
Normal file
103
src/anki-integration/ai.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { DEFAULT_ANKI_CONNECT_CONFIG } from "../config";
|
||||||
|
|
||||||
|
const DEFAULT_AI_SYSTEM_PROMPT =
|
||||||
|
"You are a translation engine. Return only the translated text with no explanations.";
|
||||||
|
|
||||||
|
export function extractAiText(content: unknown): string {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content.trim();
|
||||||
|
}
|
||||||
|
if (!Array.isArray(content)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const item of content) {
|
||||||
|
if (
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"type" in item &&
|
||||||
|
(item as { type?: unknown }).type === "text" &&
|
||||||
|
"text" in item &&
|
||||||
|
typeof (item as { text?: unknown }).text === "string"
|
||||||
|
) {
|
||||||
|
parts.push((item as { text: string }).text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeOpenAiBaseUrl(baseUrl: string): string {
|
||||||
|
const trimmed = baseUrl.trim().replace(/\/+$/, "");
|
||||||
|
if (/\/v1$/i.test(trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return `${trimmed}/v1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiTranslateRequest {
|
||||||
|
sentence: string;
|
||||||
|
apiKey: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
model?: string;
|
||||||
|
targetLanguage?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiTranslateCallbacks {
|
||||||
|
logWarning: (message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function translateSentenceWithAi(
|
||||||
|
request: AiTranslateRequest,
|
||||||
|
callbacks: AiTranslateCallbacks,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const aiConfig = DEFAULT_ANKI_CONNECT_CONFIG.ai;
|
||||||
|
if (!request.apiKey.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = normalizeOpenAiBaseUrl(
|
||||||
|
request.baseUrl || aiConfig.baseUrl || "https://openrouter.ai/api",
|
||||||
|
);
|
||||||
|
const model = request.model || "openai/gpt-4o-mini";
|
||||||
|
const targetLanguage = request.targetLanguage || "English";
|
||||||
|
const prompt =
|
||||||
|
request.systemPrompt?.trim() || DEFAULT_AI_SYSTEM_PROMPT;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseUrl}/chat/completions`,
|
||||||
|
{
|
||||||
|
model,
|
||||||
|
temperature: 0,
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: prompt },
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `Translate this text to ${targetLanguage}:\n\n${request.sentence}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${request.apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout: 15000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const content = (response.data as { choices?: unknown[] })?.choices?.[0] as
|
||||||
|
| { message?: { content?: unknown } }
|
||||||
|
| undefined;
|
||||||
|
return extractAiText(content?.message?.content) || null;
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Unknown translation error";
|
||||||
|
callbacks.logWarning(`AI translation failed: ${message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
295
src/main.ts
295
src/main.ts
@@ -94,7 +94,6 @@ import {
|
|||||||
createFieldGroupingOverlayRuntimeService,
|
createFieldGroupingOverlayRuntimeService,
|
||||||
createNumericShortcutRuntimeService,
|
createNumericShortcutRuntimeService,
|
||||||
createOverlayContentMeasurementStoreService,
|
createOverlayContentMeasurementStoreService,
|
||||||
createOverlayShortcutRuntimeHandlers,
|
|
||||||
createOverlayWindowService,
|
createOverlayWindowService,
|
||||||
createTokenizerDepsRuntimeService,
|
createTokenizerDepsRuntimeService,
|
||||||
cycleSecondarySubModeService,
|
cycleSecondarySubModeService,
|
||||||
@@ -116,9 +115,7 @@ import {
|
|||||||
mineSentenceCardService,
|
mineSentenceCardService,
|
||||||
openYomitanSettingsWindow,
|
openYomitanSettingsWindow,
|
||||||
playNextSubtitleRuntimeService,
|
playNextSubtitleRuntimeService,
|
||||||
refreshOverlayShortcutsRuntimeService,
|
|
||||||
registerGlobalShortcutsService,
|
registerGlobalShortcutsService,
|
||||||
registerOverlayShortcutsService,
|
|
||||||
replayCurrentSubtitleRuntimeService,
|
replayCurrentSubtitleRuntimeService,
|
||||||
resolveJimakuApiKeyService,
|
resolveJimakuApiKeyService,
|
||||||
runStartupBootstrapRuntimeService,
|
runStartupBootstrapRuntimeService,
|
||||||
@@ -130,34 +127,31 @@ import {
|
|||||||
setVisibleOverlayVisibleService,
|
setVisibleOverlayVisibleService,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
||||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||||
shortcutMatchesInputForLocalFallback,
|
|
||||||
showMpvOsdRuntimeService,
|
showMpvOsdRuntimeService,
|
||||||
startAppLifecycleService,
|
startAppLifecycleService,
|
||||||
syncInvisibleOverlayMousePassthroughService,
|
syncInvisibleOverlayMousePassthroughService,
|
||||||
syncOverlayShortcutsRuntimeService,
|
|
||||||
tokenizeSubtitleService,
|
tokenizeSubtitleService,
|
||||||
triggerFieldGroupingService,
|
triggerFieldGroupingService,
|
||||||
unregisterOverlayShortcutsRuntimeService,
|
|
||||||
updateCurrentMediaPathService,
|
updateCurrentMediaPathService,
|
||||||
updateInvisibleOverlayVisibilityService,
|
updateInvisibleOverlayVisibilityService,
|
||||||
updateLastCardFromClipboardService,
|
updateLastCardFromClipboardService,
|
||||||
updateVisibleOverlayVisibilityService,
|
updateVisibleOverlayVisibilityService,
|
||||||
} from "./core/services";
|
} from "./core/services";
|
||||||
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler";
|
|
||||||
import {
|
import {
|
||||||
runAppReadyRuntimeService,
|
runAppReadyRuntimeService,
|
||||||
} from "./core/services/startup-service";
|
} from "./core/services/startup-service";
|
||||||
import type { AppReadyRuntimeDeps } from "./core/services/startup-service";
|
|
||||||
import { applyRuntimeOptionResultRuntimeService } from "./core/services/runtime-options-ipc-service";
|
import { applyRuntimeOptionResultRuntimeService } from "./core/services/runtime-options-ipc-service";
|
||||||
import {
|
import {
|
||||||
createAppLifecycleRuntimeDeps as createAppLifecycleRuntimeDepsBuilder,
|
createAppLifecycleRuntimeDeps,
|
||||||
createAppReadyRuntimeDeps as createAppReadyRuntimeDepsBuilder,
|
createAppReadyRuntimeDeps,
|
||||||
} from "./main/app-lifecycle";
|
} from "./main/app-lifecycle";
|
||||||
import { handleMpvCommandFromIpcRuntime } from "./main/ipc-mpv-command";
|
import { handleMpvCommandFromIpcRuntime } from "./main/ipc-mpv-command";
|
||||||
import {
|
import {
|
||||||
registerIpcRuntimeServices,
|
registerIpcRuntimeServices,
|
||||||
} from "./main/ipc-runtime";
|
} from "./main/ipc-runtime";
|
||||||
import { handleCliCommandRuntimeService } from "./main/cli-runtime";
|
import {
|
||||||
|
handleCliCommandRuntimeServiceWithContext,
|
||||||
|
} from "./main/cli-runtime";
|
||||||
import {
|
import {
|
||||||
runSubsyncManualFromIpcRuntime,
|
runSubsyncManualFromIpcRuntime,
|
||||||
triggerSubsyncFromConfigRuntime,
|
triggerSubsyncFromConfigRuntime,
|
||||||
@@ -166,6 +160,9 @@ import {
|
|||||||
createOverlayModalRuntimeService,
|
createOverlayModalRuntimeService,
|
||||||
type OverlayHostedModal,
|
type OverlayHostedModal,
|
||||||
} from "./main/overlay-runtime";
|
} from "./main/overlay-runtime";
|
||||||
|
import {
|
||||||
|
createOverlayShortcutsRuntimeService,
|
||||||
|
} from "./main/overlay-shortcuts-runtime";
|
||||||
import {
|
import {
|
||||||
applyStartupState,
|
applyStartupState,
|
||||||
createAppState,
|
createAppState,
|
||||||
@@ -177,9 +174,6 @@ import {
|
|||||||
DEFAULT_KEYBINDINGS,
|
DEFAULT_KEYBINDINGS,
|
||||||
generateConfigTemplate,
|
generateConfigTemplate,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import type {
|
|
||||||
AppLifecycleDepsRuntimeOptions,
|
|
||||||
} from "./core/services/app-lifecycle-service";
|
|
||||||
|
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
|
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
|
||||||
@@ -290,6 +284,44 @@ const appState = createAppState({
|
|||||||
mpvSocketPath: getDefaultSocketPath(),
|
mpvSocketPath: getDefaultSocketPath(),
|
||||||
texthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
texthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||||
});
|
});
|
||||||
|
const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService({
|
||||||
|
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||||
|
getShortcutsRegistered: () => appState.shortcutsRegistered,
|
||||||
|
setShortcutsRegistered: (registered) => {
|
||||||
|
appState.shortcutsRegistered = registered;
|
||||||
|
},
|
||||||
|
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
|
openRuntimeOptionsPalette: () => {
|
||||||
|
openRuntimeOptionsPalette();
|
||||||
|
},
|
||||||
|
openJimaku: () => {
|
||||||
|
sendToActiveOverlayWindow("jimaku:open", undefined, {
|
||||||
|
restoreOnModalClose: "jimaku",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
markAudioCard: () => markLastCardAsAudioCard(),
|
||||||
|
copySubtitleMultiple: (timeoutMs) => {
|
||||||
|
startPendingMultiCopy(timeoutMs);
|
||||||
|
},
|
||||||
|
copySubtitle: () => {
|
||||||
|
copyCurrentSubtitle();
|
||||||
|
},
|
||||||
|
toggleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||||
|
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||||
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
|
mineSentenceCard: () => mineSentenceCard(),
|
||||||
|
mineSentenceMultiple: (timeoutMs) => {
|
||||||
|
startPendingMineSentenceMultiple(timeoutMs);
|
||||||
|
},
|
||||||
|
cancelPendingMultiCopy: () => {
|
||||||
|
cancelPendingMultiCopy();
|
||||||
|
},
|
||||||
|
cancelPendingMineSentenceMultiple: () => {
|
||||||
|
cancelPendingMineSentenceMultiple();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
||||||
return appState.fieldGroupingResolver;
|
return appState.fieldGroupingResolver;
|
||||||
@@ -530,79 +562,34 @@ const startupState = runStartupBootstrapRuntimeService(
|
|||||||
startAppLifecycle: (args: CliArgs) => {
|
startAppLifecycle: (args: CliArgs) => {
|
||||||
startAppLifecycleService(
|
startAppLifecycleService(
|
||||||
args,
|
args,
|
||||||
createAppLifecycleDepsRuntimeService(createAppLifecycleRuntimeDeps()),
|
createAppLifecycleDepsRuntimeService(
|
||||||
);
|
createAppLifecycleRuntimeDeps({
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
applyStartupState(appState, startupState);
|
|
||||||
|
|
||||||
function createAppLifecycleRuntimeDeps(): AppLifecycleDepsRuntimeOptions {
|
|
||||||
return createAppLifecycleRuntimeDepsBuilder({
|
|
||||||
app,
|
app,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||||
handleCliCommand: (
|
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
||||||
nextArgs: CliArgs,
|
handleCliCommand(nextArgs, source),
|
||||||
source: CliCommandSource,
|
|
||||||
) => handleCliCommand(nextArgs, source),
|
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||||
onReady: async () => {
|
onReady: async () => {
|
||||||
await runAppReadyRuntimeService(createAppReadyRuntimeDeps());
|
await runAppReadyRuntimeService(
|
||||||
},
|
createAppReadyRuntimeDeps({
|
||||||
onWillQuitCleanup: () => {
|
|
||||||
restorePreviousSecondarySubVisibility();
|
|
||||||
globalShortcut.unregisterAll();
|
|
||||||
subtitleWsService.stop();
|
|
||||||
texthookerService.stop();
|
|
||||||
if (appState.yomitanParserWindow && !appState.yomitanParserWindow.isDestroyed()) {
|
|
||||||
appState.yomitanParserWindow.destroy();
|
|
||||||
}
|
|
||||||
appState.yomitanParserWindow = null;
|
|
||||||
appState.yomitanParserReadyPromise = null;
|
|
||||||
appState.yomitanParserInitPromise = null;
|
|
||||||
if (appState.windowTracker) {
|
|
||||||
appState.windowTracker.stop();
|
|
||||||
}
|
|
||||||
if (appState.mpvClient && appState.mpvClient.socket) {
|
|
||||||
appState.mpvClient.socket.destroy();
|
|
||||||
}
|
|
||||||
if (appState.reconnectTimer) {
|
|
||||||
clearTimeout(appState.reconnectTimer);
|
|
||||||
}
|
|
||||||
if (appState.subtitleTimingTracker) {
|
|
||||||
appState.subtitleTimingTracker.destroy();
|
|
||||||
}
|
|
||||||
if (appState.ankiIntegration) {
|
|
||||||
appState.ankiIntegration.destroy();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shouldRestoreWindowsOnActivate: () =>
|
|
||||||
appState.overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,
|
|
||||||
restoreWindowsOnActivate: () => {
|
|
||||||
createMainWindow();
|
|
||||||
createInvisibleWindow();
|
|
||||||
updateVisibleOverlayVisibility();
|
|
||||||
updateInvisibleOverlayVisibility();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAppReadyRuntimeDeps(): AppReadyRuntimeDeps {
|
|
||||||
return createAppReadyRuntimeDepsBuilder({
|
|
||||||
loadSubtitlePosition: () => loadSubtitlePosition(),
|
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
resolveKeybindings: () => {
|
resolveKeybindings: () => {
|
||||||
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
appState.keybindings = resolveKeybindings(
|
||||||
|
getResolvedConfig(),
|
||||||
|
DEFAULT_KEYBINDINGS,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
createMpvClient: () => {
|
createMpvClient: () => {
|
||||||
appState.mpvClient = createMpvClientRuntimeService();
|
appState.mpvClient = createMpvClientRuntimeService();
|
||||||
},
|
},
|
||||||
reloadConfig: () => {
|
reloadConfig: () => {
|
||||||
configService.reloadConfig();
|
configService.reloadConfig();
|
||||||
appLogger.logInfo(`Using config file: ${configService.getConfigPath()}`);
|
appLogger.logInfo(
|
||||||
|
`Using config file: ${configService.getConfigPath()}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
getConfigWarnings: () => configService.getWarnings(),
|
getConfigWarnings: () => configService.getWarnings(),
|
||||||
@@ -650,26 +637,71 @@ function createAppReadyRuntimeDeps(): AppReadyRuntimeDeps {
|
|||||||
shouldAutoInitializeOverlayRuntimeFromConfig(),
|
shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
handleInitialArgs: () => handleInitialArgs(),
|
handleInitialArgs: () => handleInitialArgs(),
|
||||||
});
|
}),
|
||||||
}
|
);
|
||||||
|
},
|
||||||
|
onWillQuitCleanup: () => {
|
||||||
|
restorePreviousSecondarySubVisibility();
|
||||||
|
globalShortcut.unregisterAll();
|
||||||
|
subtitleWsService.stop();
|
||||||
|
texthookerService.stop();
|
||||||
|
if (
|
||||||
|
appState.yomitanParserWindow &&
|
||||||
|
!appState.yomitanParserWindow.isDestroyed()
|
||||||
|
) {
|
||||||
|
appState.yomitanParserWindow.destroy();
|
||||||
|
}
|
||||||
|
appState.yomitanParserWindow = null;
|
||||||
|
appState.yomitanParserReadyPromise = null;
|
||||||
|
appState.yomitanParserInitPromise = null;
|
||||||
|
if (appState.windowTracker) {
|
||||||
|
appState.windowTracker.stop();
|
||||||
|
}
|
||||||
|
if (appState.mpvClient && appState.mpvClient.socket) {
|
||||||
|
appState.mpvClient.socket.destroy();
|
||||||
|
}
|
||||||
|
if (appState.reconnectTimer) {
|
||||||
|
clearTimeout(appState.reconnectTimer);
|
||||||
|
}
|
||||||
|
if (appState.subtitleTimingTracker) {
|
||||||
|
appState.subtitleTimingTracker.destroy();
|
||||||
|
}
|
||||||
|
if (appState.ankiIntegration) {
|
||||||
|
appState.ankiIntegration.destroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldRestoreWindowsOnActivate: () =>
|
||||||
|
appState.overlayRuntimeInitialized &&
|
||||||
|
BrowserWindow.getAllWindows().length === 0,
|
||||||
|
restoreWindowsOnActivate: () => {
|
||||||
|
createMainWindow();
|
||||||
|
createInvisibleWindow();
|
||||||
|
updateVisibleOverlayVisibility();
|
||||||
|
updateInvisibleOverlayVisibility();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
applyStartupState(appState, startupState);
|
||||||
|
|
||||||
function handleCliCommand(
|
function handleCliCommand(
|
||||||
args: CliArgs,
|
args: CliArgs,
|
||||||
source: CliCommandSource = "initial",
|
source: CliCommandSource = "initial",
|
||||||
): void {
|
): void {
|
||||||
handleCliCommandRuntimeService(args, source, {
|
handleCliCommandRuntimeServiceWithContext(args, source, {
|
||||||
mpv: {
|
|
||||||
getSocketPath: () => appState.mpvSocketPath,
|
getSocketPath: () => appState.mpvSocketPath,
|
||||||
setSocketPath: (socketPath: string) => {
|
setSocketPath: (socketPath: string) => {
|
||||||
appState.mpvSocketPath = socketPath;
|
appState.mpvSocketPath = socketPath;
|
||||||
},
|
},
|
||||||
getClient: () => appState.mpvClient,
|
getClient: () => appState.mpvClient,
|
||||||
showOsd: (text: string) => showMpvOsd(text),
|
showOsd: (text: string) => showMpvOsd(text),
|
||||||
},
|
texthookerService,
|
||||||
texthooker: {
|
getTexthookerPort: () => appState.texthookerPort,
|
||||||
service: texthookerService,
|
setTexthookerPort: (port: number) => {
|
||||||
getPort: () => appState.texthookerPort,
|
|
||||||
setPort: (port: number) => {
|
|
||||||
appState.texthookerPort = port;
|
appState.texthookerPort = port;
|
||||||
},
|
},
|
||||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||||
@@ -678,16 +710,12 @@ function handleCliCommand(
|
|||||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
isOverlayInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
overlay: {
|
initializeOverlay: () => initializeOverlayRuntime(),
|
||||||
isInitialized: () => appState.overlayRuntimeInitialized,
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
initialize: () => initializeOverlayRuntime(),
|
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
||||||
toggleVisible: () => toggleVisibleOverlay(),
|
setVisibleOverlay: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||||
toggleInvisible: () => toggleInvisibleOverlay(),
|
setInvisibleOverlay: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||||
setVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
|
||||||
setInvisible: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
|
||||||
},
|
|
||||||
mining: {
|
|
||||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||||
mineSentenceCard: () => mineSentenceCard(),
|
mineSentenceCard: () => mineSentenceCard(),
|
||||||
@@ -697,17 +725,12 @@ function handleCliCommand(
|
|||||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
},
|
stopApp: () => app.quit(),
|
||||||
app: {
|
|
||||||
stop: () => app.quit(),
|
|
||||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||||
},
|
|
||||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||||
log: (message: string) => {
|
log: (message: string) => {
|
||||||
@@ -875,7 +898,7 @@ function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
|
|||||||
? overlayManager.getVisibleOverlayVisible()
|
? overlayManager.getVisibleOverlayVisible()
|
||||||
: overlayManager.getInvisibleOverlayVisible(),
|
: overlayManager.getInvisibleOverlayVisible(),
|
||||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||||
tryHandleOverlayShortcutLocalFallback(input),
|
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||||
onWindowClosed: (windowKind) => {
|
onWindowClosed: (windowKind) => {
|
||||||
if (windowKind === "visible") {
|
if (windowKind === "visible") {
|
||||||
overlayManager.setMainWindow(null);
|
overlayManager.setMainWindow(null);
|
||||||
@@ -932,9 +955,7 @@ function initializeOverlayRuntime(): void {
|
|||||||
updateInvisibleOverlayVisibility();
|
updateInvisibleOverlayVisibility();
|
||||||
},
|
},
|
||||||
getOverlayWindows: () => getOverlayWindows(),
|
getOverlayWindows: () => getOverlayWindows(),
|
||||||
syncOverlayShortcuts: () => {
|
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||||
syncOverlayShortcuts();
|
|
||||||
},
|
|
||||||
setWindowTracker: (tracker) => {
|
setWindowTracker: (tracker) => {
|
||||||
appState.windowTracker = tracker;
|
appState.windowTracker = tracker;
|
||||||
},
|
},
|
||||||
@@ -980,46 +1001,6 @@ function registerGlobalShortcuts(): void {
|
|||||||
|
|
||||||
function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); }
|
function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolvedConfig(), DEFAULT_CONFIG); }
|
||||||
|
|
||||||
function getOverlayShortcutRuntimeHandlers() {
|
|
||||||
return createOverlayShortcutRuntimeHandlers(
|
|
||||||
{
|
|
||||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
|
||||||
openRuntimeOptions: () => {
|
|
||||||
openRuntimeOptionsPalette();
|
|
||||||
},
|
|
||||||
openJimaku: () => {
|
|
||||||
sendToActiveOverlayWindow("jimaku:open", undefined, {
|
|
||||||
restoreOnModalClose: "jimaku",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
markAudioCard: () => markLastCardAsAudioCard(),
|
|
||||||
copySubtitleMultiple: (timeoutMs: number) => {
|
|
||||||
startPendingMultiCopy(timeoutMs);
|
|
||||||
},
|
|
||||||
copySubtitle: () => {
|
|
||||||
copyCurrentSubtitle();
|
|
||||||
},
|
|
||||||
toggleSecondarySub: () => cycleSecondarySubMode(),
|
|
||||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
|
||||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
|
||||||
triggerSubsync: () => triggerSubsyncFromConfig(),
|
|
||||||
mineSentence: () => mineSentenceCard(),
|
|
||||||
mineSentenceMultiple: (timeoutMs: number) => {
|
|
||||||
startPendingMineSentenceMultiple(timeoutMs);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
|
||||||
return runOverlayShortcutLocalFallback(
|
|
||||||
input,
|
|
||||||
getConfiguredShortcuts(),
|
|
||||||
shortcutMatchesInputForLocalFallback,
|
|
||||||
getOverlayShortcutRuntimeHandlers().fallbackHandlers,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cycleSecondarySubMode(): void {
|
function cycleSecondarySubMode(): void {
|
||||||
cycleSecondarySubModeService(
|
cycleSecondarySubModeService(
|
||||||
{
|
{
|
||||||
@@ -1201,42 +1182,18 @@ function handleMineSentenceDigit(count: number): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function registerOverlayShortcuts(): void {
|
function registerOverlayShortcuts(): void {
|
||||||
appState.shortcutsRegistered = registerOverlayShortcutsService(
|
overlayShortcutsRuntime.registerOverlayShortcuts();
|
||||||
getConfiguredShortcuts(),
|
|
||||||
getOverlayShortcutRuntimeHandlers().overlayHandlers,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOverlayShortcutLifecycleDeps() {
|
|
||||||
return {
|
|
||||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
|
||||||
getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers,
|
|
||||||
cancelPendingMultiCopy: () => cancelPendingMultiCopy(),
|
|
||||||
cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unregisterOverlayShortcuts(): void {
|
function unregisterOverlayShortcuts(): void {
|
||||||
appState.shortcutsRegistered = unregisterOverlayShortcutsRuntimeService(
|
overlayShortcutsRuntime.unregisterOverlayShortcuts();
|
||||||
appState.shortcutsRegistered,
|
|
||||||
getOverlayShortcutLifecycleDeps(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldOverlayShortcutsBeActive(): boolean { return appState.overlayRuntimeInitialized; }
|
|
||||||
function syncOverlayShortcuts(): void {
|
function syncOverlayShortcuts(): void {
|
||||||
appState.shortcutsRegistered = syncOverlayShortcutsRuntimeService(
|
overlayShortcutsRuntime.syncOverlayShortcuts();
|
||||||
shouldOverlayShortcutsBeActive(),
|
|
||||||
appState.shortcutsRegistered,
|
|
||||||
getOverlayShortcutLifecycleDeps(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
function refreshOverlayShortcuts(): void {
|
function refreshOverlayShortcuts(): void {
|
||||||
appState.shortcutsRegistered = refreshOverlayShortcutsRuntimeService(
|
overlayShortcutsRuntime.refreshOverlayShortcuts();
|
||||||
shouldOverlayShortcutsBeActive(),
|
|
||||||
appState.shortcutsRegistered,
|
|
||||||
getOverlayShortcutLifecycleDeps(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVisibleOverlayVisibility(): void {
|
function updateVisibleOverlayVisibility(): void {
|
||||||
|
|||||||
@@ -2,6 +2,99 @@ import { handleCliCommandService, createCliCommandDepsRuntimeService } from "../
|
|||||||
import type { CliArgs, CliCommandSource } from "../cli/args";
|
import type { CliArgs, CliCommandSource } from "../cli/args";
|
||||||
import { createCliCommandRuntimeServiceDeps, CliCommandRuntimeServiceDepsParams } from "./dependencies";
|
import { createCliCommandRuntimeServiceDeps, CliCommandRuntimeServiceDepsParams } from "./dependencies";
|
||||||
|
|
||||||
|
export interface CliCommandRuntimeServiceContext {
|
||||||
|
getSocketPath: () => string;
|
||||||
|
setSocketPath: (socketPath: string) => void;
|
||||||
|
getClient: CliCommandRuntimeServiceDepsParams["mpv"]["getClient"];
|
||||||
|
showOsd: CliCommandRuntimeServiceDepsParams["mpv"]["showOsd"];
|
||||||
|
getTexthookerPort: () => number;
|
||||||
|
setTexthookerPort: (port: number) => void;
|
||||||
|
shouldOpenBrowser: () => boolean;
|
||||||
|
openInBrowser: (url: string) => void;
|
||||||
|
isOverlayInitialized: () => boolean;
|
||||||
|
initializeOverlay: () => void;
|
||||||
|
toggleVisibleOverlay: () => void;
|
||||||
|
toggleInvisibleOverlay: () => void;
|
||||||
|
setVisibleOverlay: (visible: boolean) => void;
|
||||||
|
setInvisibleOverlay: (visible: boolean) => void;
|
||||||
|
copyCurrentSubtitle: () => void;
|
||||||
|
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||||
|
mineSentenceCard: () => Promise<void>;
|
||||||
|
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||||
|
updateLastCardFromClipboard: () => Promise<void>;
|
||||||
|
triggerFieldGrouping: () => Promise<void>;
|
||||||
|
triggerSubsyncFromConfig: () => Promise<void>;
|
||||||
|
markLastCardAsAudioCard: () => Promise<void>;
|
||||||
|
openYomitanSettings: () => void;
|
||||||
|
cycleSecondarySubMode: () => void;
|
||||||
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
printHelp: () => void;
|
||||||
|
stopApp: () => void;
|
||||||
|
hasMainWindow: () => boolean;
|
||||||
|
getMultiCopyTimeoutMs: () => number;
|
||||||
|
schedule: (fn: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
||||||
|
log: (message: string) => void;
|
||||||
|
warn: (message: string) => void;
|
||||||
|
error: (message: string, err: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CliCommandRuntimeServiceContextHandlers {
|
||||||
|
texthookerService: CliCommandRuntimeServiceDepsParams["texthooker"]["service"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCliCommandDepsFromContext(
|
||||||
|
context: CliCommandRuntimeServiceContext & CliCommandRuntimeServiceContextHandlers,
|
||||||
|
): CliCommandRuntimeServiceDepsParams {
|
||||||
|
return {
|
||||||
|
mpv: {
|
||||||
|
getSocketPath: context.getSocketPath,
|
||||||
|
setSocketPath: context.setSocketPath,
|
||||||
|
getClient: context.getClient,
|
||||||
|
showOsd: context.showOsd,
|
||||||
|
},
|
||||||
|
texthooker: {
|
||||||
|
service: context.texthookerService,
|
||||||
|
getPort: context.getTexthookerPort,
|
||||||
|
setPort: context.setTexthookerPort,
|
||||||
|
shouldOpenBrowser: context.shouldOpenBrowser,
|
||||||
|
openInBrowser: context.openInBrowser,
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
isInitialized: context.isOverlayInitialized,
|
||||||
|
initialize: context.initializeOverlay,
|
||||||
|
toggleVisible: context.toggleVisibleOverlay,
|
||||||
|
toggleInvisible: context.toggleInvisibleOverlay,
|
||||||
|
setVisible: context.setVisibleOverlay,
|
||||||
|
setInvisible: context.setInvisibleOverlay,
|
||||||
|
},
|
||||||
|
mining: {
|
||||||
|
copyCurrentSubtitle: context.copyCurrentSubtitle,
|
||||||
|
startPendingMultiCopy: context.startPendingMultiCopy,
|
||||||
|
mineSentenceCard: context.mineSentenceCard,
|
||||||
|
startPendingMineSentenceMultiple: context.startPendingMineSentenceMultiple,
|
||||||
|
updateLastCardFromClipboard: context.updateLastCardFromClipboard,
|
||||||
|
triggerFieldGrouping: context.triggerFieldGrouping,
|
||||||
|
triggerSubsyncFromConfig: context.triggerSubsyncFromConfig,
|
||||||
|
markLastCardAsAudioCard: context.markLastCardAsAudioCard,
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
openYomitanSettings: context.openYomitanSettings,
|
||||||
|
cycleSecondarySubMode: context.cycleSecondarySubMode,
|
||||||
|
openRuntimeOptionsPalette: context.openRuntimeOptionsPalette,
|
||||||
|
printHelp: context.printHelp,
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
stop: context.stopApp,
|
||||||
|
hasMainWindow: context.hasMainWindow,
|
||||||
|
},
|
||||||
|
getMultiCopyTimeoutMs: context.getMultiCopyTimeoutMs,
|
||||||
|
schedule: context.schedule,
|
||||||
|
log: context.log,
|
||||||
|
warn: context.warn,
|
||||||
|
error: context.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function handleCliCommandRuntimeService(
|
export function handleCliCommandRuntimeService(
|
||||||
args: CliArgs,
|
args: CliArgs,
|
||||||
source: CliCommandSource,
|
source: CliCommandSource,
|
||||||
@@ -13,3 +106,10 @@ export function handleCliCommandRuntimeService(
|
|||||||
handleCliCommandService(args, source, deps);
|
handleCliCommandService(args, source, deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleCliCommandRuntimeServiceWithContext(
|
||||||
|
args: CliArgs,
|
||||||
|
source: CliCommandSource,
|
||||||
|
context: CliCommandRuntimeServiceContext & CliCommandRuntimeServiceContextHandlers,
|
||||||
|
): void {
|
||||||
|
handleCliCommandRuntimeService(args, source, createCliCommandDepsFromContext(context));
|
||||||
|
}
|
||||||
|
|||||||
138
src/main/overlay-shortcuts-runtime.ts
Normal file
138
src/main/overlay-shortcuts-runtime.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { ConfiguredShortcuts } from "../core/utils/shortcut-config";
|
||||||
|
import {
|
||||||
|
createOverlayShortcutRuntimeHandlers,
|
||||||
|
shortcutMatchesInputForLocalFallback,
|
||||||
|
} from "../core/services";
|
||||||
|
import {
|
||||||
|
refreshOverlayShortcutsRuntimeService,
|
||||||
|
registerOverlayShortcutsService,
|
||||||
|
syncOverlayShortcutsRuntimeService,
|
||||||
|
unregisterOverlayShortcutsRuntimeService,
|
||||||
|
} from "../core/services";
|
||||||
|
import { runOverlayShortcutLocalFallback } from "../core/services/overlay-shortcut-handler";
|
||||||
|
|
||||||
|
export interface OverlayShortcutRuntimeServiceInput {
|
||||||
|
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||||
|
getShortcutsRegistered: () => boolean;
|
||||||
|
setShortcutsRegistered: (registered: boolean) => void;
|
||||||
|
isOverlayRuntimeInitialized: () => boolean;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
openJimaku: () => void;
|
||||||
|
markAudioCard: () => Promise<void>;
|
||||||
|
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||||
|
copySubtitle: () => void;
|
||||||
|
toggleSecondarySubMode: () => void;
|
||||||
|
updateLastCardFromClipboard: () => Promise<void>;
|
||||||
|
triggerFieldGrouping: () => Promise<void>;
|
||||||
|
triggerSubsyncFromConfig: () => Promise<void>;
|
||||||
|
mineSentenceCard: () => Promise<void>;
|
||||||
|
mineSentenceMultiple: (timeoutMs: number) => void;
|
||||||
|
cancelPendingMultiCopy: () => void;
|
||||||
|
cancelPendingMineSentenceMultiple: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OverlayShortcutsRuntimeService {
|
||||||
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
|
registerOverlayShortcuts: () => void;
|
||||||
|
unregisterOverlayShortcuts: () => void;
|
||||||
|
syncOverlayShortcuts: () => void;
|
||||||
|
refreshOverlayShortcuts: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOverlayShortcutsRuntimeService(
|
||||||
|
input: OverlayShortcutRuntimeServiceInput,
|
||||||
|
): OverlayShortcutsRuntimeService {
|
||||||
|
const handlers = createOverlayShortcutRuntimeHandlers({
|
||||||
|
showMpvOsd: (text: string) => input.showMpvOsd(text),
|
||||||
|
openRuntimeOptions: () => {
|
||||||
|
input.openRuntimeOptionsPalette();
|
||||||
|
},
|
||||||
|
openJimaku: () => {
|
||||||
|
input.openJimaku();
|
||||||
|
},
|
||||||
|
markAudioCard: () => {
|
||||||
|
return input.markAudioCard();
|
||||||
|
},
|
||||||
|
copySubtitleMultiple: (timeoutMs: number) => {
|
||||||
|
input.copySubtitleMultiple(timeoutMs);
|
||||||
|
},
|
||||||
|
copySubtitle: () => {
|
||||||
|
input.copySubtitle();
|
||||||
|
},
|
||||||
|
toggleSecondarySub: () => {
|
||||||
|
input.toggleSecondarySubMode();
|
||||||
|
},
|
||||||
|
updateLastCardFromClipboard: () => {
|
||||||
|
return input.updateLastCardFromClipboard();
|
||||||
|
},
|
||||||
|
triggerFieldGrouping: () => {
|
||||||
|
return input.triggerFieldGrouping();
|
||||||
|
},
|
||||||
|
triggerSubsync: () => {
|
||||||
|
return input.triggerSubsyncFromConfig();
|
||||||
|
},
|
||||||
|
mineSentence: () => {
|
||||||
|
return input.mineSentenceCard();
|
||||||
|
},
|
||||||
|
mineSentenceMultiple: (timeoutMs: number) => {
|
||||||
|
input.mineSentenceMultiple(timeoutMs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getShortcutLifecycleDeps = () => {
|
||||||
|
return {
|
||||||
|
getConfiguredShortcuts: () => input.getConfiguredShortcuts(),
|
||||||
|
getOverlayHandlers: () => handlers.overlayHandlers,
|
||||||
|
cancelPendingMultiCopy: () => input.cancelPendingMultiCopy(),
|
||||||
|
cancelPendingMineSentenceMultiple: () =>
|
||||||
|
input.cancelPendingMineSentenceMultiple(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldOverlayShortcutsBeActive = () => input.isOverlayRuntimeInitialized();
|
||||||
|
|
||||||
|
return {
|
||||||
|
tryHandleOverlayShortcutLocalFallback: (inputEvent) =>
|
||||||
|
runOverlayShortcutLocalFallback(
|
||||||
|
inputEvent,
|
||||||
|
input.getConfiguredShortcuts(),
|
||||||
|
shortcutMatchesInputForLocalFallback,
|
||||||
|
handlers.fallbackHandlers,
|
||||||
|
),
|
||||||
|
registerOverlayShortcuts: () => {
|
||||||
|
input.setShortcutsRegistered(
|
||||||
|
registerOverlayShortcutsService(
|
||||||
|
input.getConfiguredShortcuts(),
|
||||||
|
handlers.overlayHandlers,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
unregisterOverlayShortcuts: () => {
|
||||||
|
input.setShortcutsRegistered(
|
||||||
|
unregisterOverlayShortcutsRuntimeService(
|
||||||
|
input.getShortcutsRegistered(),
|
||||||
|
getShortcutLifecycleDeps(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
syncOverlayShortcuts: () => {
|
||||||
|
input.setShortcutsRegistered(
|
||||||
|
syncOverlayShortcutsRuntimeService(
|
||||||
|
shouldOverlayShortcutsBeActive(),
|
||||||
|
input.getShortcutsRegistered(),
|
||||||
|
getShortcutLifecycleDeps(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
refreshOverlayShortcuts: () => {
|
||||||
|
input.setShortcutsRegistered(
|
||||||
|
refreshOverlayShortcutsRuntimeService(
|
||||||
|
shouldOverlayShortcutsBeActive(),
|
||||||
|
input.getShortcutsRegistered(),
|
||||||
|
getShortcutLifecycleDeps(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user