mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor(core): consolidate services and remove runtime wrappers
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createAppLifecycleDepsRuntimeService } from "./app-lifecycle-deps-runtime-service";
|
||||
|
||||
test("createAppLifecycleDepsRuntimeService maps app methods and platform", async () => {
|
||||
const events: Record<string, (...args: unknown[]) => void> = {};
|
||||
let lockRequested = 0;
|
||||
let quitCalled = 0;
|
||||
const deps = createAppLifecycleDepsRuntimeService({
|
||||
app: {
|
||||
requestSingleInstanceLock: () => {
|
||||
lockRequested += 1;
|
||||
return true;
|
||||
},
|
||||
quit: () => {
|
||||
quitCalled += 1;
|
||||
},
|
||||
on: (event, listener) => {
|
||||
events[event] = listener;
|
||||
},
|
||||
whenReady: async () => {},
|
||||
},
|
||||
platform: "darwin",
|
||||
shouldStartApp: () => true,
|
||||
parseArgs: () => ({
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
toggleVisibleOverlay: false,
|
||||
toggleInvisibleOverlay: false,
|
||||
settings: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
showVisibleOverlay: false,
|
||||
hideVisibleOverlay: false,
|
||||
showInvisibleOverlay: false,
|
||||
hideInvisibleOverlay: false,
|
||||
copySubtitle: false,
|
||||
copySubtitleMultiple: false,
|
||||
mineSentence: false,
|
||||
mineSentenceMultiple: false,
|
||||
updateLastCardFromClipboard: false,
|
||||
toggleSecondarySub: false,
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
openRuntimeOptions: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
generateConfig: false,
|
||||
backupOverwrite: false,
|
||||
verbose: false,
|
||||
}),
|
||||
handleCliCommand: () => {},
|
||||
printHelp: () => {},
|
||||
logNoRunningInstance: () => {},
|
||||
onReady: async () => {},
|
||||
onWillQuitCleanup: () => {},
|
||||
shouldRestoreWindowsOnActivate: () => false,
|
||||
restoreWindowsOnActivate: () => {},
|
||||
});
|
||||
|
||||
assert.equal(deps.requestSingleInstanceLock(), true);
|
||||
deps.quitApp();
|
||||
assert.equal(lockRequested, 1);
|
||||
assert.equal(quitCalled, 1);
|
||||
assert.equal(deps.isDarwinPlatform(), true);
|
||||
|
||||
let callbackRan = false;
|
||||
deps.whenReady(async () => {
|
||||
callbackRan = true;
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(callbackRan, true);
|
||||
|
||||
deps.onActivate(() => {});
|
||||
assert.equal(typeof events["activate"], "function");
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { CliArgs, CliCommandSource } from "../../cli/args";
|
||||
import { AppLifecycleServiceDeps } from "./app-lifecycle-service";
|
||||
|
||||
interface AppLike {
|
||||
requestSingleInstanceLock: () => boolean;
|
||||
quit: () => void;
|
||||
on: (...args: any[]) => unknown;
|
||||
whenReady: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface AppLifecycleDepsRuntimeOptions {
|
||||
app: AppLike;
|
||||
platform: NodeJS.Platform;
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
handleCliCommand: (args: CliArgs, source: CliCommandSource) => void;
|
||||
printHelp: () => void;
|
||||
logNoRunningInstance: () => void;
|
||||
onReady: () => Promise<void>;
|
||||
onWillQuitCleanup: () => void;
|
||||
shouldRestoreWindowsOnActivate: () => boolean;
|
||||
restoreWindowsOnActivate: () => void;
|
||||
}
|
||||
|
||||
export function createAppLifecycleDepsRuntimeService(
|
||||
options: AppLifecycleDepsRuntimeOptions,
|
||||
): AppLifecycleServiceDeps {
|
||||
return {
|
||||
shouldStartApp: options.shouldStartApp,
|
||||
parseArgs: options.parseArgs,
|
||||
requestSingleInstanceLock: () => options.app.requestSingleInstanceLock(),
|
||||
quitApp: () => options.app.quit(),
|
||||
onSecondInstance: (handler) => {
|
||||
options.app.on("second-instance", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
handleCliCommand: options.handleCliCommand,
|
||||
printHelp: options.printHelp,
|
||||
logNoRunningInstance: options.logNoRunningInstance,
|
||||
whenReady: (handler) => {
|
||||
options.app.whenReady().then(handler);
|
||||
},
|
||||
onWindowAllClosed: (handler) => {
|
||||
options.app.on("window-all-closed", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
onWillQuit: (handler) => {
|
||||
options.app.on("will-quit", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
onActivate: (handler) => {
|
||||
options.app.on("activate", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
isDarwinPlatform: () => options.platform === "darwin",
|
||||
onReady: options.onReady,
|
||||
onWillQuitCleanup: options.onWillQuitCleanup,
|
||||
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: options.restoreWindowsOnActivate,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,63 @@ export interface AppLifecycleServiceDeps {
|
||||
restoreWindowsOnActivate: () => void;
|
||||
}
|
||||
|
||||
interface AppLike {
|
||||
requestSingleInstanceLock: () => boolean;
|
||||
quit: () => void;
|
||||
on: (...args: any[]) => unknown;
|
||||
whenReady: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface AppLifecycleDepsRuntimeOptions {
|
||||
app: AppLike;
|
||||
platform: NodeJS.Platform;
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
handleCliCommand: (args: CliArgs, source: CliCommandSource) => void;
|
||||
printHelp: () => void;
|
||||
logNoRunningInstance: () => void;
|
||||
onReady: () => Promise<void>;
|
||||
onWillQuitCleanup: () => void;
|
||||
shouldRestoreWindowsOnActivate: () => boolean;
|
||||
restoreWindowsOnActivate: () => void;
|
||||
}
|
||||
|
||||
export function createAppLifecycleDepsRuntimeService(
|
||||
options: AppLifecycleDepsRuntimeOptions,
|
||||
): AppLifecycleServiceDeps {
|
||||
return {
|
||||
shouldStartApp: options.shouldStartApp,
|
||||
parseArgs: options.parseArgs,
|
||||
requestSingleInstanceLock: () => options.app.requestSingleInstanceLock(),
|
||||
quitApp: () => options.app.quit(),
|
||||
onSecondInstance: (handler) => {
|
||||
options.app.on("second-instance", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
handleCliCommand: options.handleCliCommand,
|
||||
printHelp: options.printHelp,
|
||||
logNoRunningInstance: options.logNoRunningInstance,
|
||||
whenReady: (handler) => {
|
||||
options.app.whenReady().then(handler).catch((error) => {
|
||||
console.error("App ready handler failed:", error);
|
||||
});
|
||||
},
|
||||
onWindowAllClosed: (handler) => {
|
||||
options.app.on("window-all-closed", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
onWillQuit: (handler) => {
|
||||
options.app.on("will-quit", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
onActivate: (handler) => {
|
||||
options.app.on("activate", handler as (...args: unknown[]) => void);
|
||||
},
|
||||
isDarwinPlatform: () => options.platform === "darwin",
|
||||
onReady: options.onReady,
|
||||
onWillQuitCleanup: options.onWillQuitCleanup,
|
||||
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: options.restoreWindowsOnActivate,
|
||||
};
|
||||
}
|
||||
|
||||
export function startAppLifecycleService(
|
||||
initialArgs: CliArgs,
|
||||
deps: AppLifecycleServiceDeps,
|
||||
@@ -31,7 +88,11 @@ export function startAppLifecycleService(
|
||||
}
|
||||
|
||||
deps.onSecondInstance((_event, argv) => {
|
||||
deps.handleCliCommand(deps.parseArgs(argv), "second-instance");
|
||||
try {
|
||||
deps.handleCliCommand(deps.parseArgs(argv), "second-instance");
|
||||
} catch (error) {
|
||||
console.error("Failed to handle second-instance CLI command:", error);
|
||||
}
|
||||
});
|
||||
|
||||
if (initialArgs.help && !deps.shouldStartApp(initialArgs)) {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createAppLoggingRuntimeService } from "./app-logging-runtime-service";
|
||||
|
||||
test("createAppLoggingRuntimeService routes logs and formats config warnings", () => {
|
||||
const lines: string[] = [];
|
||||
const logger = {
|
||||
log: (line: string) => lines.push(`log:${line}`),
|
||||
warn: (line: string) => lines.push(`warn:${line}`),
|
||||
error: (line: string) => lines.push(`error:${line}`),
|
||||
};
|
||||
|
||||
const runtime = createAppLoggingRuntimeService(logger);
|
||||
runtime.logInfo("hello");
|
||||
runtime.logWarning("careful");
|
||||
runtime.logNoRunningInstance();
|
||||
runtime.logConfigWarning({
|
||||
path: "x.y",
|
||||
value: "bad",
|
||||
fallback: "good",
|
||||
message: "invalid",
|
||||
});
|
||||
|
||||
assert.equal(lines[0], "log:hello");
|
||||
assert.equal(lines[1], "warn:careful");
|
||||
assert.equal(lines[2], "error:No running instance. Use --start to launch the app.");
|
||||
assert.match(lines[3], /^warn:\[config\] x\.y: invalid /);
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ConfigValidationWarning } from "../../types";
|
||||
import { logConfigWarningRuntimeService } from "./config-warning-runtime-service";
|
||||
|
||||
export interface AppLoggingRuntime {
|
||||
logInfo: (message: string) => void;
|
||||
logWarning: (message: string) => void;
|
||||
logNoRunningInstance: () => void;
|
||||
logConfigWarning: (warning: ConfigValidationWarning) => void;
|
||||
}
|
||||
|
||||
export function createAppLoggingRuntimeService(
|
||||
logger: Pick<Console, "log" | "warn" | "error"> = console,
|
||||
): AppLoggingRuntime {
|
||||
return {
|
||||
logInfo: (message) => {
|
||||
logger.log(message);
|
||||
},
|
||||
logWarning: (message) => {
|
||||
logger.warn(message);
|
||||
},
|
||||
logNoRunningInstance: () => {
|
||||
logger.error("No running instance. Use --start to launch the app.");
|
||||
},
|
||||
logConfigWarning: (warning) => {
|
||||
logConfigWarningRuntimeService(warning, (line) => logger.warn(line));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { AppReadyRuntimeDeps, runAppReadyRuntimeService } from "./app-ready-runtime-service";
|
||||
import { AppReadyRuntimeDeps, runAppReadyRuntimeService } from "./startup-service";
|
||||
|
||||
function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||
const calls: string[] = [];
|
||||
@@ -1,32 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { runAppShutdownRuntimeService } from "./app-shutdown-runtime-service";
|
||||
|
||||
test("runAppShutdownRuntimeService runs teardown steps in order", () => {
|
||||
const calls: string[] = [];
|
||||
runAppShutdownRuntimeService({
|
||||
unregisterAllGlobalShortcuts: () => calls.push("unregisterAllGlobalShortcuts"),
|
||||
stopSubtitleWebsocket: () => calls.push("stopSubtitleWebsocket"),
|
||||
stopTexthookerService: () => calls.push("stopTexthookerService"),
|
||||
destroyYomitanParserWindow: () => calls.push("destroyYomitanParserWindow"),
|
||||
clearYomitanParserPromises: () => calls.push("clearYomitanParserPromises"),
|
||||
stopWindowTracker: () => calls.push("stopWindowTracker"),
|
||||
destroyMpvSocket: () => calls.push("destroyMpvSocket"),
|
||||
clearReconnectTimer: () => calls.push("clearReconnectTimer"),
|
||||
destroySubtitleTimingTracker: () => calls.push("destroySubtitleTimingTracker"),
|
||||
destroyAnkiIntegration: () => calls.push("destroyAnkiIntegration"),
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"unregisterAllGlobalShortcuts",
|
||||
"stopSubtitleWebsocket",
|
||||
"stopTexthookerService",
|
||||
"destroyYomitanParserWindow",
|
||||
"clearYomitanParserPromises",
|
||||
"stopWindowTracker",
|
||||
"destroyMpvSocket",
|
||||
"clearReconnectTimer",
|
||||
"destroySubtitleTimingTracker",
|
||||
"destroyAnkiIntegration",
|
||||
]);
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
export interface AppShutdownRuntimeDeps {
|
||||
unregisterAllGlobalShortcuts: () => void;
|
||||
stopSubtitleWebsocket: () => void;
|
||||
stopTexthookerService: () => void;
|
||||
destroyYomitanParserWindow: () => void;
|
||||
clearYomitanParserPromises: () => void;
|
||||
stopWindowTracker: () => void;
|
||||
destroyMpvSocket: () => void;
|
||||
clearReconnectTimer: () => void;
|
||||
destroySubtitleTimingTracker: () => void;
|
||||
destroyAnkiIntegration: () => void;
|
||||
}
|
||||
|
||||
export function runAppShutdownRuntimeService(
|
||||
deps: AppShutdownRuntimeDeps,
|
||||
): void {
|
||||
deps.unregisterAllGlobalShortcuts();
|
||||
deps.stopSubtitleWebsocket();
|
||||
deps.stopTexthookerService();
|
||||
deps.destroyYomitanParserWindow();
|
||||
deps.clearYomitanParserPromises();
|
||||
deps.stopWindowTracker();
|
||||
deps.destroyMpvSocket();
|
||||
deps.clearReconnectTimer();
|
||||
deps.destroySubtitleTimingTracker();
|
||||
deps.destroyAnkiIntegration();
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createCliCommandDepsRuntimeService } from "./cli-command-deps-runtime-service";
|
||||
|
||||
test("createCliCommandDepsRuntimeService wires runtime helpers", () => {
|
||||
let socketPath = "/tmp/mpv";
|
||||
let setClientSocketPath: string | null = null;
|
||||
let connectCalls = 0;
|
||||
let texthookerPort = 7000;
|
||||
let texthookerRunning = false;
|
||||
let texthookerStartPort: number | null = null;
|
||||
let overlayVisible = false;
|
||||
let overlayInvisible = false;
|
||||
let openYomitanAfterDelay: number | null = null;
|
||||
|
||||
const deps = createCliCommandDepsRuntimeService({
|
||||
mpv: {
|
||||
getSocketPath: () => socketPath,
|
||||
setSocketPath: (next) => {
|
||||
socketPath = next;
|
||||
},
|
||||
getClient: () => ({
|
||||
setSocketPath: (next) => {
|
||||
setClientSocketPath = next;
|
||||
},
|
||||
connect: () => {
|
||||
connectCalls += 1;
|
||||
},
|
||||
}),
|
||||
showOsd: () => {},
|
||||
},
|
||||
texthooker: {
|
||||
service: {
|
||||
isRunning: () => texthookerRunning,
|
||||
start: (port) => {
|
||||
texthookerRunning = true;
|
||||
texthookerStartPort = port;
|
||||
},
|
||||
},
|
||||
getPort: () => texthookerPort,
|
||||
setPort: (port) => {
|
||||
texthookerPort = port;
|
||||
},
|
||||
shouldOpenBrowser: () => true,
|
||||
openInBrowser: () => {},
|
||||
},
|
||||
overlay: {
|
||||
isInitialized: () => false,
|
||||
initialize: () => {},
|
||||
toggleVisible: () => {
|
||||
overlayVisible = !overlayVisible;
|
||||
},
|
||||
toggleInvisible: () => {
|
||||
overlayInvisible = !overlayInvisible;
|
||||
},
|
||||
setVisible: (visible) => {
|
||||
overlayVisible = visible;
|
||||
},
|
||||
setInvisible: (visible) => {
|
||||
overlayInvisible = visible;
|
||||
},
|
||||
},
|
||||
mining: {
|
||||
copyCurrentSubtitle: () => {},
|
||||
startPendingMultiCopy: () => {},
|
||||
mineSentenceCard: async () => {},
|
||||
startPendingMineSentenceMultiple: () => {},
|
||||
updateLastCardFromClipboard: async () => {},
|
||||
triggerFieldGrouping: async () => {},
|
||||
triggerSubsyncFromConfig: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
},
|
||||
ui: {
|
||||
openYomitanSettings: () => {},
|
||||
cycleSecondarySubMode: () => {},
|
||||
openRuntimeOptionsPalette: () => {},
|
||||
printHelp: () => {},
|
||||
},
|
||||
app: {
|
||||
stop: () => {},
|
||||
hasMainWindow: () => true,
|
||||
},
|
||||
getMultiCopyTimeoutMs: () => 2500,
|
||||
schedule: (_fn, delayMs) => {
|
||||
openYomitanAfterDelay = delayMs;
|
||||
return null;
|
||||
},
|
||||
log: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
});
|
||||
|
||||
deps.setMpvSocketPath("/tmp/new");
|
||||
deps.setMpvClientSocketPath("/tmp/new");
|
||||
deps.connectMpvClient();
|
||||
deps.ensureTexthookerRunning(9000);
|
||||
deps.openYomitanSettingsDelayed(1000);
|
||||
deps.toggleVisibleOverlay();
|
||||
deps.toggleInvisibleOverlay();
|
||||
|
||||
assert.equal(deps.getMpvSocketPath(), "/tmp/new");
|
||||
assert.equal(setClientSocketPath, "/tmp/new");
|
||||
assert.equal(connectCalls, 1);
|
||||
assert.equal(texthookerStartPort, 9000);
|
||||
assert.equal(texthookerPort, 7000);
|
||||
assert.equal(openYomitanAfterDelay, 1000);
|
||||
assert.equal(overlayVisible, true);
|
||||
assert.equal(overlayInvisible, true);
|
||||
assert.equal(deps.getMultiCopyTimeoutMs(), 2500);
|
||||
});
|
||||
@@ -1,132 +0,0 @@
|
||||
import { CliCommandServiceDeps } from "./cli-command-service";
|
||||
|
||||
interface MpvClientLike {
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
connect: () => void;
|
||||
}
|
||||
|
||||
interface TexthookerServiceLike {
|
||||
isRunning: () => boolean;
|
||||
start: (port: number) => void;
|
||||
}
|
||||
|
||||
interface MpvCliRuntime {
|
||||
getSocketPath: () => string;
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
getClient: () => MpvClientLike | null;
|
||||
showOsd: (text: string) => void;
|
||||
}
|
||||
|
||||
interface TexthookerCliRuntime {
|
||||
service: TexthookerServiceLike;
|
||||
getPort: () => number;
|
||||
setPort: (port: number) => void;
|
||||
shouldOpenBrowser: () => boolean;
|
||||
openInBrowser: (url: string) => void;
|
||||
}
|
||||
|
||||
interface OverlayCliRuntime {
|
||||
isInitialized: () => boolean;
|
||||
initialize: () => void;
|
||||
toggleVisible: () => void;
|
||||
toggleInvisible: () => void;
|
||||
setVisible: (visible: boolean) => void;
|
||||
setInvisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
interface MiningCliRuntime {
|
||||
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>;
|
||||
}
|
||||
|
||||
interface UiCliRuntime {
|
||||
openYomitanSettings: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
printHelp: () => void;
|
||||
}
|
||||
|
||||
interface AppCliRuntime {
|
||||
stop: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
}
|
||||
|
||||
export interface CliCommandDepsRuntimeOptions {
|
||||
mpv: MpvCliRuntime;
|
||||
texthooker: TexthookerCliRuntime;
|
||||
overlay: OverlayCliRuntime;
|
||||
mining: MiningCliRuntime;
|
||||
ui: UiCliRuntime;
|
||||
app: AppCliRuntime;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
schedule: (fn: () => void, delayMs: number) => unknown;
|
||||
log: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string, err: unknown) => void;
|
||||
}
|
||||
|
||||
export function createCliCommandDepsRuntimeService(
|
||||
options: CliCommandDepsRuntimeOptions,
|
||||
): CliCommandServiceDeps {
|
||||
return {
|
||||
getMpvSocketPath: options.mpv.getSocketPath,
|
||||
setMpvSocketPath: options.mpv.setSocketPath,
|
||||
setMpvClientSocketPath: (socketPath) => {
|
||||
const client = options.mpv.getClient();
|
||||
if (!client) return;
|
||||
client.setSocketPath(socketPath);
|
||||
},
|
||||
hasMpvClient: () => Boolean(options.mpv.getClient()),
|
||||
connectMpvClient: () => {
|
||||
const client = options.mpv.getClient();
|
||||
if (!client) return;
|
||||
client.connect();
|
||||
},
|
||||
isTexthookerRunning: () => options.texthooker.service.isRunning(),
|
||||
setTexthookerPort: options.texthooker.setPort,
|
||||
getTexthookerPort: options.texthooker.getPort,
|
||||
shouldOpenTexthookerBrowser: options.texthooker.shouldOpenBrowser,
|
||||
ensureTexthookerRunning: (port) => {
|
||||
if (!options.texthooker.service.isRunning()) {
|
||||
options.texthooker.service.start(port);
|
||||
}
|
||||
},
|
||||
openTexthookerInBrowser: options.texthooker.openInBrowser,
|
||||
stopApp: options.app.stop,
|
||||
isOverlayRuntimeInitialized: options.overlay.isInitialized,
|
||||
initializeOverlayRuntime: options.overlay.initialize,
|
||||
toggleVisibleOverlay: options.overlay.toggleVisible,
|
||||
toggleInvisibleOverlay: options.overlay.toggleInvisible,
|
||||
openYomitanSettingsDelayed: (delayMs) => {
|
||||
options.schedule(() => {
|
||||
options.ui.openYomitanSettings();
|
||||
}, delayMs);
|
||||
},
|
||||
setVisibleOverlayVisible: options.overlay.setVisible,
|
||||
setInvisibleOverlayVisible: options.overlay.setInvisible,
|
||||
copyCurrentSubtitle: options.mining.copyCurrentSubtitle,
|
||||
startPendingMultiCopy: options.mining.startPendingMultiCopy,
|
||||
mineSentenceCard: options.mining.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple:
|
||||
options.mining.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: options.mining.updateLastCardFromClipboard,
|
||||
cycleSecondarySubMode: options.ui.cycleSecondarySubMode,
|
||||
triggerFieldGrouping: options.mining.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: options.mining.markLastCardAsAudioCard,
|
||||
openRuntimeOptionsPalette: options.ui.openRuntimeOptionsPalette,
|
||||
printHelp: options.ui.printHelp,
|
||||
hasMainWindow: options.app.hasMainWindow,
|
||||
getMultiCopyTimeoutMs: options.getMultiCopyTimeoutMs,
|
||||
showMpvOsd: options.mpv.showOsd,
|
||||
log: options.log,
|
||||
warn: options.warn,
|
||||
error: options.error,
|
||||
};
|
||||
}
|
||||
@@ -43,6 +43,137 @@ export interface CliCommandServiceDeps {
|
||||
error: (message: string, err: unknown) => void;
|
||||
}
|
||||
|
||||
interface MpvClientLike {
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
connect: () => void;
|
||||
}
|
||||
|
||||
interface TexthookerServiceLike {
|
||||
isRunning: () => boolean;
|
||||
start: (port: number) => void;
|
||||
}
|
||||
|
||||
interface MpvCliRuntime {
|
||||
getSocketPath: () => string;
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
getClient: () => MpvClientLike | null;
|
||||
showOsd: (text: string) => void;
|
||||
}
|
||||
|
||||
interface TexthookerCliRuntime {
|
||||
service: TexthookerServiceLike;
|
||||
getPort: () => number;
|
||||
setPort: (port: number) => void;
|
||||
shouldOpenBrowser: () => boolean;
|
||||
openInBrowser: (url: string) => void;
|
||||
}
|
||||
|
||||
interface OverlayCliRuntime {
|
||||
isInitialized: () => boolean;
|
||||
initialize: () => void;
|
||||
toggleVisible: () => void;
|
||||
toggleInvisible: () => void;
|
||||
setVisible: (visible: boolean) => void;
|
||||
setInvisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
interface MiningCliRuntime {
|
||||
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>;
|
||||
}
|
||||
|
||||
interface UiCliRuntime {
|
||||
openYomitanSettings: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
printHelp: () => void;
|
||||
}
|
||||
|
||||
interface AppCliRuntime {
|
||||
stop: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
}
|
||||
|
||||
export interface CliCommandDepsRuntimeOptions {
|
||||
mpv: MpvCliRuntime;
|
||||
texthooker: TexthookerCliRuntime;
|
||||
overlay: OverlayCliRuntime;
|
||||
mining: MiningCliRuntime;
|
||||
ui: UiCliRuntime;
|
||||
app: AppCliRuntime;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
schedule: (fn: () => void, delayMs: number) => unknown;
|
||||
log: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string, err: unknown) => void;
|
||||
}
|
||||
|
||||
export function createCliCommandDepsRuntimeService(
|
||||
options: CliCommandDepsRuntimeOptions,
|
||||
): CliCommandServiceDeps {
|
||||
return {
|
||||
getMpvSocketPath: options.mpv.getSocketPath,
|
||||
setMpvSocketPath: options.mpv.setSocketPath,
|
||||
setMpvClientSocketPath: (socketPath) => {
|
||||
const client = options.mpv.getClient();
|
||||
if (!client) return;
|
||||
client.setSocketPath(socketPath);
|
||||
},
|
||||
hasMpvClient: () => Boolean(options.mpv.getClient()),
|
||||
connectMpvClient: () => {
|
||||
const client = options.mpv.getClient();
|
||||
if (!client) return;
|
||||
client.connect();
|
||||
},
|
||||
isTexthookerRunning: () => options.texthooker.service.isRunning(),
|
||||
setTexthookerPort: options.texthooker.setPort,
|
||||
getTexthookerPort: options.texthooker.getPort,
|
||||
shouldOpenTexthookerBrowser: options.texthooker.shouldOpenBrowser,
|
||||
ensureTexthookerRunning: (port) => {
|
||||
if (!options.texthooker.service.isRunning()) {
|
||||
options.texthooker.service.start(port);
|
||||
}
|
||||
},
|
||||
openTexthookerInBrowser: options.texthooker.openInBrowser,
|
||||
stopApp: options.app.stop,
|
||||
isOverlayRuntimeInitialized: options.overlay.isInitialized,
|
||||
initializeOverlayRuntime: options.overlay.initialize,
|
||||
toggleVisibleOverlay: options.overlay.toggleVisible,
|
||||
toggleInvisibleOverlay: options.overlay.toggleInvisible,
|
||||
openYomitanSettingsDelayed: (delayMs) => {
|
||||
options.schedule(() => {
|
||||
options.ui.openYomitanSettings();
|
||||
}, delayMs);
|
||||
},
|
||||
setVisibleOverlayVisible: options.overlay.setVisible,
|
||||
setInvisibleOverlayVisible: options.overlay.setInvisible,
|
||||
copyCurrentSubtitle: options.mining.copyCurrentSubtitle,
|
||||
startPendingMultiCopy: options.mining.startPendingMultiCopy,
|
||||
mineSentenceCard: options.mining.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple:
|
||||
options.mining.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: options.mining.updateLastCardFromClipboard,
|
||||
cycleSecondarySubMode: options.ui.cycleSecondarySubMode,
|
||||
triggerFieldGrouping: options.mining.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: options.mining.markLastCardAsAudioCard,
|
||||
openRuntimeOptionsPalette: options.ui.openRuntimeOptionsPalette,
|
||||
printHelp: options.ui.printHelp,
|
||||
hasMainWindow: options.app.hasMainWindow,
|
||||
getMultiCopyTimeoutMs: options.getMultiCopyTimeoutMs,
|
||||
showMpvOsd: options.mpv.showOsd,
|
||||
log: options.log,
|
||||
warn: options.warn,
|
||||
error: options.error,
|
||||
};
|
||||
}
|
||||
|
||||
function runAsyncWithOsd(
|
||||
task: () => Promise<void>,
|
||||
deps: CliCommandServiceDeps,
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { runGenerateConfigFlowRuntimeService } from "./config-generation-runtime-service";
|
||||
import { CliArgs } from "../../cli/args";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
start: false,
|
||||
stop: false,
|
||||
toggle: false,
|
||||
toggleVisibleOverlay: false,
|
||||
toggleInvisibleOverlay: false,
|
||||
settings: false,
|
||||
show: false,
|
||||
hide: false,
|
||||
showVisibleOverlay: false,
|
||||
hideVisibleOverlay: false,
|
||||
showInvisibleOverlay: false,
|
||||
hideInvisibleOverlay: false,
|
||||
copySubtitle: false,
|
||||
copySubtitleMultiple: false,
|
||||
mineSentence: false,
|
||||
mineSentenceMultiple: false,
|
||||
updateLastCardFromClipboard: false,
|
||||
toggleSecondarySub: false,
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
openRuntimeOptions: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
autoStartOverlay: false,
|
||||
generateConfig: false,
|
||||
backupOverwrite: false,
|
||||
verbose: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("runGenerateConfigFlowRuntimeService starts flow when generateConfig is set and app should not start", async () => {
|
||||
const calls: string[] = [];
|
||||
const handled = runGenerateConfigFlowRuntimeService(
|
||||
makeArgs({ generateConfig: true }),
|
||||
{
|
||||
shouldStartApp: () => false,
|
||||
generateConfig: async () => 7,
|
||||
onSuccess: (code) => calls.push(`success:${code}`),
|
||||
onError: () => calls.push("error"),
|
||||
},
|
||||
);
|
||||
assert.equal(handled, true);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.deepEqual(calls, ["success:7"]);
|
||||
});
|
||||
|
||||
test("runGenerateConfigFlowRuntimeService returns false when flow should not run", () => {
|
||||
const handled = runGenerateConfigFlowRuntimeService(
|
||||
makeArgs({ generateConfig: true, start: true }),
|
||||
{
|
||||
shouldStartApp: () => true,
|
||||
generateConfig: async () => 0,
|
||||
onSuccess: () => {},
|
||||
onError: () => {},
|
||||
},
|
||||
);
|
||||
assert.equal(handled, false);
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { CliArgs } from "../../cli/args";
|
||||
|
||||
export interface ConfigGenerationRuntimeDeps {
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
generateConfig: (args: CliArgs) => Promise<number>;
|
||||
onSuccess: (exitCode: number) => void;
|
||||
onError: (error: Error) => void;
|
||||
}
|
||||
|
||||
export function runGenerateConfigFlowRuntimeService(
|
||||
args: CliArgs,
|
||||
deps: ConfigGenerationRuntimeDeps,
|
||||
): boolean {
|
||||
if (!args.generateConfig || deps.shouldStartApp(args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
deps.generateConfig(args)
|
||||
.then((exitCode) => {
|
||||
deps.onSuccess(exitCode);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
deps.onError(error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
formatConfigWarningRuntimeService,
|
||||
logConfigWarningRuntimeService,
|
||||
} from "./config-warning-runtime-service";
|
||||
|
||||
test("formatConfigWarningRuntimeService formats warning line", () => {
|
||||
const message = formatConfigWarningRuntimeService({
|
||||
path: "ankiConnect.enabled",
|
||||
value: "oops",
|
||||
fallback: true,
|
||||
message: "invalid type",
|
||||
});
|
||||
assert.equal(
|
||||
message,
|
||||
'[config] ankiConnect.enabled: invalid type value="oops" fallback=true',
|
||||
);
|
||||
});
|
||||
|
||||
test("logConfigWarningRuntimeService delegates to logger", () => {
|
||||
const logs: string[] = [];
|
||||
logConfigWarningRuntimeService(
|
||||
{
|
||||
path: "x.y",
|
||||
value: 1,
|
||||
fallback: 2,
|
||||
message: "bad",
|
||||
},
|
||||
(line) => logs.push(line),
|
||||
);
|
||||
assert.equal(logs.length, 1);
|
||||
assert.match(logs[0], /^\[config\] x\.y: bad /);
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ConfigValidationWarning } from "../../types";
|
||||
|
||||
export function formatConfigWarningRuntimeService(
|
||||
warning: ConfigValidationWarning,
|
||||
): string {
|
||||
return `[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`;
|
||||
}
|
||||
|
||||
export function logConfigWarningRuntimeService(
|
||||
warning: ConfigValidationWarning,
|
||||
log: (message: string) => void,
|
||||
): void {
|
||||
log(formatConfigWarningRuntimeService(warning));
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { KikuFieldGroupingChoice } from "../../types";
|
||||
import { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-runtime-service";
|
||||
import { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
|
||||
|
||||
test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets restore flag", () => {
|
||||
const sent: unknown[][] = [];
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import {
|
||||
createFieldGroupingCallbackRuntimeService,
|
||||
sendToVisibleOverlayRuntimeService,
|
||||
} from "./overlay-bridge-runtime-service";
|
||||
} from "./overlay-bridge-service";
|
||||
|
||||
interface WindowLike {
|
||||
isDestroyed: () => boolean;
|
||||
@@ -16,6 +16,16 @@ export function createFieldGroupingCallbackService(options: {
|
||||
data: KikuFieldGroupingRequestData,
|
||||
): Promise<KikuFieldGroupingChoice> => {
|
||||
return new Promise((resolve) => {
|
||||
if (options.getResolver()) {
|
||||
resolve({
|
||||
keepNoteId: 0,
|
||||
deleteNoteId: 0,
|
||||
deleteDuplicate: true,
|
||||
cancelled: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const previousVisibleOverlay = options.getVisibleOverlayVisible();
|
||||
const previousInvisibleOverlay = options.getInvisibleOverlayVisible();
|
||||
let settled = false;
|
||||
@@ -23,7 +33,9 @@ export function createFieldGroupingCallbackService(options: {
|
||||
const finish = (choice: KikuFieldGroupingChoice): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
options.setResolver(null);
|
||||
if (options.getResolver() === finish) {
|
||||
options.setResolver(null);
|
||||
}
|
||||
resolve(choice);
|
||||
|
||||
if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export { TexthookerService } from "./texthooker-service";
|
||||
export { hasMpvWebsocketPlugin, SubtitleWebSocketService } from "./subtitle-ws-service";
|
||||
export { registerGlobalShortcutsService } from "./shortcut-service";
|
||||
export { registerIpcHandlersService } from "./ipc-service";
|
||||
export { createIpcDepsRuntimeService, registerIpcHandlersService } from "./ipc-service";
|
||||
export { isGlobalShortcutRegisteredSafe, shortcutMatchesInputForLocalFallback } from "./shortcut-fallback-service";
|
||||
export { registerOverlayShortcutsService } from "./overlay-shortcut-service";
|
||||
export { createOverlayShortcutRuntimeHandlers } from "./overlay-shortcut-runtime-service";
|
||||
export { handleCliCommandService } from "./cli-command-service";
|
||||
export { cycleSecondarySubModeService } from "./secondary-subtitle-service";
|
||||
export {
|
||||
refreshOverlayShortcutsRuntimeService,
|
||||
registerOverlayShortcutsService,
|
||||
syncOverlayShortcutsRuntimeService,
|
||||
unregisterOverlayShortcutsRuntimeService,
|
||||
} from "./overlay-shortcut-lifecycle-service";
|
||||
} from "./overlay-shortcut-service";
|
||||
export { createOverlayShortcutRuntimeHandlers } from "./overlay-shortcut-handler";
|
||||
export { createCliCommandDepsRuntimeService, handleCliCommandService } from "./cli-command-service";
|
||||
export { cycleSecondarySubModeService } from "./secondary-subtitle-service";
|
||||
export {
|
||||
copyCurrentSubtitleService,
|
||||
handleMineSentenceDigitService,
|
||||
@@ -20,15 +20,15 @@ export {
|
||||
mineSentenceCardService,
|
||||
triggerFieldGroupingService,
|
||||
updateLastCardFromClipboardService,
|
||||
} from "./mining-runtime-service";
|
||||
export { startAppLifecycleService } from "./app-lifecycle-service";
|
||||
} from "./mining-service";
|
||||
export { createAppLifecycleDepsRuntimeService, startAppLifecycleService } from "./app-lifecycle-service";
|
||||
export {
|
||||
playNextSubtitleRuntimeService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-runtime-service";
|
||||
} from "./mpv-control-service";
|
||||
export {
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
@@ -36,14 +36,14 @@ export {
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||
} from "./runtime-config-service";
|
||||
export { openYomitanSettingsWindow } from "./yomitan-settings-service";
|
||||
export { tokenizeSubtitleService } from "./tokenizer-service";
|
||||
export { createTokenizerDepsRuntimeService, tokenizeSubtitleService } from "./tokenizer-service";
|
||||
export { loadYomitanExtensionService } from "./yomitan-extension-loader-service";
|
||||
export {
|
||||
getJimakuLanguagePreferenceService,
|
||||
getJimakuMaxEntryResultsService,
|
||||
jimakuFetchJsonService,
|
||||
resolveJimakuApiKeyService,
|
||||
} from "./jimaku-runtime-service";
|
||||
} from "./jimaku-service";
|
||||
export {
|
||||
loadSubtitlePositionService,
|
||||
saveSubtitlePositionService,
|
||||
@@ -60,33 +60,19 @@ export {
|
||||
setInvisibleOverlayVisibleService,
|
||||
setVisibleOverlayVisibleService,
|
||||
syncInvisibleOverlayMousePassthroughService,
|
||||
} from "./overlay-visibility-runtime-service";
|
||||
updateInvisibleOverlayVisibilityService,
|
||||
updateVisibleOverlayVisibilityService,
|
||||
} from "./overlay-visibility-service";
|
||||
export { MpvIpcClient, MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from "./mpv-service";
|
||||
export { applyMpvSubtitleRenderMetricsPatchService } from "./mpv-render-metrics-service";
|
||||
export { handleMpvCommandFromIpcService } from "./ipc-command-service";
|
||||
export { handleOverlayModalClosedService } from "./overlay-modal-restore-service";
|
||||
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
|
||||
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-service";
|
||||
export { runStartupBootstrapRuntimeService } from "./startup-service";
|
||||
export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runner-service";
|
||||
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-service";
|
||||
export {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
broadcastToOverlayWindowsRuntimeService,
|
||||
getOverlayWindowsRuntimeService,
|
||||
createOverlayManagerService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./overlay-broadcast-runtime-service";
|
||||
export { createAppLifecycleDepsRuntimeService } from "./app-lifecycle-deps-runtime-service";
|
||||
export { createCliCommandDepsRuntimeService } from "./cli-command-deps-runtime-service";
|
||||
export { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service";
|
||||
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-runtime-service";
|
||||
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-runtime-service";
|
||||
export { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service";
|
||||
export { runOverlayShortcutLocalFallbackRuntimeService } from "./shortcut-ui-deps-runtime-service";
|
||||
export { createRuntimeOptionsManagerRuntimeService } from "./runtime-options-manager-runtime-service";
|
||||
export { createAppLoggingRuntimeService } from "./app-logging-runtime-service";
|
||||
export {
|
||||
createMecabTokenizerAndCheckRuntimeService,
|
||||
createSubtitleTimingTrackerRuntimeService,
|
||||
} from "./startup-resource-runtime-service";
|
||||
export { runGenerateConfigFlowRuntimeService } from "./config-generation-runtime-service";
|
||||
export { runStartupBootstrapRuntimeService } from "./startup-bootstrap-runtime-service";
|
||||
export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runtime-service";
|
||||
export { updateInvisibleOverlayVisibilityService, updateVisibleOverlayVisibilityService } from "./overlay-visibility-service";
|
||||
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-runtime-service";
|
||||
export { createOverlayManagerService } from "./overlay-manager-service";
|
||||
} from "./overlay-manager-service";
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service";
|
||||
|
||||
test("createIpcDepsRuntimeService maps window and mecab helpers", async () => {
|
||||
let ignoreMouse: { ignore: boolean; forward?: boolean } | null = null;
|
||||
let toggledDevTools = 0;
|
||||
let mecabEnabled: boolean | null = null;
|
||||
|
||||
const visibleWindow = {
|
||||
isDestroyed: () => false,
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||
ignoreMouse = { ignore, forward: options?.forward };
|
||||
},
|
||||
webContents: {
|
||||
toggleDevTools: () => {
|
||||
toggledDevTools += 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const deps = createIpcDepsRuntimeService({
|
||||
getInvisibleWindow: () => visibleWindow,
|
||||
getMainWindow: () => visibleWindow,
|
||||
getVisibleOverlayVisibility: () => true,
|
||||
getInvisibleOverlayVisibility: () => false,
|
||||
onOverlayModalClosed: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
quitApp: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
tokenizeCurrentSubtitle: async () => ({ text: "x" }),
|
||||
getCurrentSubtitleAss: () => "ass",
|
||||
getMpvSubtitleRenderMetrics: () => ({ subPos: 100 }),
|
||||
getSubtitlePosition: () => ({ x: 1, y: 2 }),
|
||||
getSubtitleStyle: () => null,
|
||||
saveSubtitlePosition: () => {},
|
||||
getMecabTokenizer: () => ({
|
||||
getStatus: () => ({ available: true, enabled: true, path: "/usr/bin/mecab" }),
|
||||
setEnabled: (enabled: boolean) => {
|
||||
mecabEnabled = enabled;
|
||||
},
|
||||
}),
|
||||
handleMpvCommand: () => {},
|
||||
getKeybindings: () => ({ copySubtitle: ["C"] }),
|
||||
getSecondarySubMode: () => "hidden",
|
||||
getMpvClient: () => ({ currentSecondarySubText: "secondary" }),
|
||||
runSubsyncManual: async () => ({ ok: true }),
|
||||
getAnkiConnectStatus: () => true,
|
||||
getRuntimeOptions: () => ({ values: {} }),
|
||||
setRuntimeOption: () => ({ ok: true }),
|
||||
cycleRuntimeOption: () => ({ ok: true }),
|
||||
});
|
||||
|
||||
deps.setInvisibleIgnoreMouseEvents(true, { forward: true });
|
||||
deps.toggleDevTools();
|
||||
deps.setMecabEnabled(false);
|
||||
|
||||
assert.deepEqual(ignoreMouse, { ignore: true, forward: true });
|
||||
assert.equal(toggledDevTools, 1);
|
||||
assert.equal(mecabEnabled, false);
|
||||
assert.deepEqual(deps.getMecabStatus(), {
|
||||
available: true,
|
||||
enabled: true,
|
||||
path: "/usr/bin/mecab",
|
||||
});
|
||||
assert.equal(deps.getCurrentSecondarySub(), "secondary");
|
||||
assert.deepEqual(await deps.tokenizeCurrentSubtitle(), { text: "x" });
|
||||
});
|
||||
|
||||
test("createIpcDepsRuntimeService handles missing optional runtime resources", () => {
|
||||
const deps = createIpcDepsRuntimeService({
|
||||
getInvisibleWindow: () => null,
|
||||
getMainWindow: () => null,
|
||||
getVisibleOverlayVisibility: () => false,
|
||||
getInvisibleOverlayVisibility: () => false,
|
||||
onOverlayModalClosed: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
quitApp: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
tokenizeCurrentSubtitle: async () => null,
|
||||
getCurrentSubtitleAss: () => "",
|
||||
getMpvSubtitleRenderMetrics: () => null,
|
||||
getSubtitlePosition: () => null,
|
||||
getSubtitleStyle: () => null,
|
||||
saveSubtitlePosition: () => {},
|
||||
getMecabTokenizer: () => null,
|
||||
handleMpvCommand: () => {},
|
||||
getKeybindings: () => null,
|
||||
getSecondarySubMode: () => "hidden",
|
||||
getMpvClient: () => null,
|
||||
runSubsyncManual: async () => ({ ok: false }),
|
||||
getAnkiConnectStatus: () => false,
|
||||
getRuntimeOptions: () => null,
|
||||
setRuntimeOption: () => ({ ok: false }),
|
||||
cycleRuntimeOption: () => ({ ok: false }),
|
||||
});
|
||||
|
||||
deps.setInvisibleIgnoreMouseEvents(true, { forward: true });
|
||||
deps.toggleDevTools();
|
||||
deps.setMecabEnabled(true);
|
||||
|
||||
assert.deepEqual(deps.getMecabStatus(), {
|
||||
available: false,
|
||||
enabled: false,
|
||||
path: null,
|
||||
});
|
||||
assert.equal(deps.getCurrentSecondarySub(), "");
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
import { IpcServiceDeps } from "./ipc-service";
|
||||
|
||||
interface WindowLike {
|
||||
isDestroyed: () => boolean;
|
||||
setIgnoreMouseEvents: (
|
||||
ignore: boolean,
|
||||
options?: { forward?: boolean },
|
||||
) => void;
|
||||
webContents: {
|
||||
toggleDevTools: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface MecabTokenizerLike {
|
||||
getStatus: () => { available: boolean; enabled: boolean; path: string | null };
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
interface MpvClientLike {
|
||||
currentSecondarySubText?: string;
|
||||
}
|
||||
|
||||
export interface IpcDepsRuntimeOptions {
|
||||
getInvisibleWindow: () => WindowLike | null;
|
||||
getMainWindow: () => WindowLike | null;
|
||||
getVisibleOverlayVisibility: () => boolean;
|
||||
getInvisibleOverlayVisibility: () => boolean;
|
||||
onOverlayModalClosed: (modal: string) => void;
|
||||
openYomitanSettings: () => void;
|
||||
quitApp: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
tokenizeCurrentSubtitle: () => Promise<unknown>;
|
||||
getCurrentSubtitleAss: () => string;
|
||||
getMpvSubtitleRenderMetrics: () => unknown;
|
||||
getSubtitlePosition: () => unknown;
|
||||
getSubtitleStyle: () => unknown;
|
||||
saveSubtitlePosition: (position: unknown) => void;
|
||||
getMecabTokenizer: () => MecabTokenizerLike | null;
|
||||
handleMpvCommand: (command: Array<string | number>) => void;
|
||||
getKeybindings: () => unknown;
|
||||
getSecondarySubMode: () => unknown;
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
runSubsyncManual: (request: unknown) => Promise<unknown>;
|
||||
getAnkiConnectStatus: () => boolean;
|
||||
getRuntimeOptions: () => unknown;
|
||||
setRuntimeOption: (id: string, value: unknown) => unknown;
|
||||
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
|
||||
}
|
||||
|
||||
export function createIpcDepsRuntimeService(
|
||||
options: IpcDepsRuntimeOptions,
|
||||
): IpcServiceDeps {
|
||||
return {
|
||||
getInvisibleWindow: () => options.getInvisibleWindow() as never,
|
||||
isVisibleOverlayVisible: options.getVisibleOverlayVisibility,
|
||||
setInvisibleIgnoreMouseEvents: (ignore, eventsOptions) => {
|
||||
const invisibleWindow = options.getInvisibleWindow();
|
||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
||||
invisibleWindow.setIgnoreMouseEvents(ignore, eventsOptions);
|
||||
},
|
||||
onOverlayModalClosed: options.onOverlayModalClosed,
|
||||
openYomitanSettings: options.openYomitanSettings,
|
||||
quitApp: options.quitApp,
|
||||
toggleDevTools: () => {
|
||||
const mainWindow = options.getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
mainWindow.webContents.toggleDevTools();
|
||||
},
|
||||
getVisibleOverlayVisibility: options.getVisibleOverlayVisibility,
|
||||
toggleVisibleOverlay: options.toggleVisibleOverlay,
|
||||
getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility,
|
||||
tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle,
|
||||
getCurrentSubtitleAss: options.getCurrentSubtitleAss,
|
||||
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
|
||||
getSubtitlePosition: options.getSubtitlePosition,
|
||||
getSubtitleStyle: options.getSubtitleStyle,
|
||||
saveSubtitlePosition: options.saveSubtitlePosition,
|
||||
getMecabStatus: () => {
|
||||
const mecabTokenizer = options.getMecabTokenizer();
|
||||
return mecabTokenizer
|
||||
? mecabTokenizer.getStatus()
|
||||
: { available: false, enabled: false, path: null };
|
||||
},
|
||||
setMecabEnabled: (enabled) => {
|
||||
const mecabTokenizer = options.getMecabTokenizer();
|
||||
if (!mecabTokenizer) return;
|
||||
mecabTokenizer.setEnabled(enabled);
|
||||
},
|
||||
handleMpvCommand: options.handleMpvCommand,
|
||||
getKeybindings: options.getKeybindings,
|
||||
getSecondarySubMode: options.getSecondarySubMode,
|
||||
getCurrentSecondarySub: () =>
|
||||
options.getMpvClient()?.currentSecondarySubText || "",
|
||||
runSubsyncManual: options.runSubsyncManual,
|
||||
getAnkiConnectStatus: options.getAnkiConnectStatus,
|
||||
getRuntimeOptions: options.getRuntimeOptions,
|
||||
setRuntimeOption: options.setRuntimeOption,
|
||||
cycleRuntimeOption: options.cycleRuntimeOption,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BrowserWindow, ipcMain, IpcMainEvent } from "electron";
|
||||
|
||||
export interface IpcServiceDeps {
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
getInvisibleWindow: () => WindowLike | null;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
||||
onOverlayModalClosed: (modal: string) => void;
|
||||
@@ -30,6 +30,105 @@ export interface IpcServiceDeps {
|
||||
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
|
||||
}
|
||||
|
||||
interface WindowLike {
|
||||
isDestroyed: () => boolean;
|
||||
setIgnoreMouseEvents: (
|
||||
ignore: boolean,
|
||||
options?: { forward?: boolean },
|
||||
) => void;
|
||||
webContents: {
|
||||
toggleDevTools: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface MecabTokenizerLike {
|
||||
getStatus: () => { available: boolean; enabled: boolean; path: string | null };
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
interface MpvClientLike {
|
||||
currentSecondarySubText?: string;
|
||||
}
|
||||
|
||||
export interface IpcDepsRuntimeOptions {
|
||||
getInvisibleWindow: () => WindowLike | null;
|
||||
getMainWindow: () => WindowLike | null;
|
||||
getVisibleOverlayVisibility: () => boolean;
|
||||
getInvisibleOverlayVisibility: () => boolean;
|
||||
onOverlayModalClosed: (modal: string) => void;
|
||||
openYomitanSettings: () => void;
|
||||
quitApp: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
tokenizeCurrentSubtitle: () => Promise<unknown>;
|
||||
getCurrentSubtitleAss: () => string;
|
||||
getMpvSubtitleRenderMetrics: () => unknown;
|
||||
getSubtitlePosition: () => unknown;
|
||||
getSubtitleStyle: () => unknown;
|
||||
saveSubtitlePosition: (position: unknown) => void;
|
||||
getMecabTokenizer: () => MecabTokenizerLike | null;
|
||||
handleMpvCommand: (command: Array<string | number>) => void;
|
||||
getKeybindings: () => unknown;
|
||||
getSecondarySubMode: () => unknown;
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
runSubsyncManual: (request: unknown) => Promise<unknown>;
|
||||
getAnkiConnectStatus: () => boolean;
|
||||
getRuntimeOptions: () => unknown;
|
||||
setRuntimeOption: (id: string, value: unknown) => unknown;
|
||||
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
|
||||
}
|
||||
|
||||
export function createIpcDepsRuntimeService(
|
||||
options: IpcDepsRuntimeOptions,
|
||||
): IpcServiceDeps {
|
||||
return {
|
||||
getInvisibleWindow: () => options.getInvisibleWindow(),
|
||||
isVisibleOverlayVisible: options.getVisibleOverlayVisibility,
|
||||
setInvisibleIgnoreMouseEvents: (ignore, eventsOptions) => {
|
||||
const invisibleWindow = options.getInvisibleWindow();
|
||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
||||
invisibleWindow.setIgnoreMouseEvents(ignore, eventsOptions);
|
||||
},
|
||||
onOverlayModalClosed: options.onOverlayModalClosed,
|
||||
openYomitanSettings: options.openYomitanSettings,
|
||||
quitApp: options.quitApp,
|
||||
toggleDevTools: () => {
|
||||
const mainWindow = options.getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
mainWindow.webContents.toggleDevTools();
|
||||
},
|
||||
getVisibleOverlayVisibility: options.getVisibleOverlayVisibility,
|
||||
toggleVisibleOverlay: options.toggleVisibleOverlay,
|
||||
getInvisibleOverlayVisibility: options.getInvisibleOverlayVisibility,
|
||||
tokenizeCurrentSubtitle: options.tokenizeCurrentSubtitle,
|
||||
getCurrentSubtitleAss: options.getCurrentSubtitleAss,
|
||||
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
|
||||
getSubtitlePosition: options.getSubtitlePosition,
|
||||
getSubtitleStyle: options.getSubtitleStyle,
|
||||
saveSubtitlePosition: options.saveSubtitlePosition,
|
||||
getMecabStatus: () => {
|
||||
const mecabTokenizer = options.getMecabTokenizer();
|
||||
return mecabTokenizer
|
||||
? mecabTokenizer.getStatus()
|
||||
: { available: false, enabled: false, path: null };
|
||||
},
|
||||
setMecabEnabled: (enabled) => {
|
||||
const mecabTokenizer = options.getMecabTokenizer();
|
||||
if (!mecabTokenizer) return;
|
||||
mecabTokenizer.setEnabled(enabled);
|
||||
},
|
||||
handleMpvCommand: options.handleMpvCommand,
|
||||
getKeybindings: options.getKeybindings,
|
||||
getSecondarySubMode: options.getSecondarySubMode,
|
||||
getCurrentSecondarySub: () =>
|
||||
options.getMpvClient()?.currentSecondarySubText || "",
|
||||
runSubsyncManual: options.runSubsyncManual,
|
||||
getAnkiConnectStatus: options.getAnkiConnectStatus,
|
||||
getRuntimeOptions: options.getRuntimeOptions,
|
||||
setRuntimeOption: options.setRuntimeOption,
|
||||
cycleRuntimeOption: options.cycleRuntimeOption,
|
||||
};
|
||||
}
|
||||
|
||||
export function registerIpcHandlersService(deps: IpcServiceDeps): void {
|
||||
ipcMain.on(
|
||||
"set-ignore-mouse-events",
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-runtime-service";
|
||||
} from "./mpv-control-service";
|
||||
|
||||
test("showMpvOsdRuntimeService sends show-text when connected", () => {
|
||||
const commands: (string | number)[][] = [];
|
||||
@@ -1,48 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createNumericShortcutRuntimeService } from "./numeric-shortcut-runtime-service";
|
||||
|
||||
test("createNumericShortcutRuntimeService creates sessions wired to globalShortcut", () => {
|
||||
const registered: string[] = [];
|
||||
const unregistered: string[] = [];
|
||||
const osd: string[] = [];
|
||||
const handlers = new Map<string, () => void>();
|
||||
|
||||
const runtime = createNumericShortcutRuntimeService({
|
||||
globalShortcut: {
|
||||
register: (accelerator, callback) => {
|
||||
registered.push(accelerator);
|
||||
handlers.set(accelerator, callback);
|
||||
return true;
|
||||
},
|
||||
unregister: (accelerator) => {
|
||||
unregistered.push(accelerator);
|
||||
handlers.delete(accelerator);
|
||||
},
|
||||
},
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
setTimer: () => setTimeout(() => {}, 1000),
|
||||
clearTimer: (timer) => clearTimeout(timer),
|
||||
});
|
||||
|
||||
const session = runtime.createSession();
|
||||
session.start({
|
||||
timeoutMs: 5000,
|
||||
onDigit: () => {},
|
||||
messages: {
|
||||
prompt: "Select count",
|
||||
timeout: "Timed out",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(session.isActive(), true);
|
||||
assert.ok(registered.includes("1"));
|
||||
assert.ok(registered.includes("Escape"));
|
||||
assert.equal(osd[0], "Select count");
|
||||
|
||||
handlers.get("Escape")?.();
|
||||
assert.equal(session.isActive(), false);
|
||||
assert.ok(unregistered.includes("Escape"));
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import {
|
||||
createNumericShortcutSessionService,
|
||||
} from "./numeric-shortcut-session-service";
|
||||
|
||||
interface GlobalShortcutLike {
|
||||
register: (accelerator: string, callback: () => void) => boolean;
|
||||
unregister: (accelerator: string) => void;
|
||||
}
|
||||
|
||||
export interface NumericShortcutRuntimeOptions {
|
||||
globalShortcut: GlobalShortcutLike;
|
||||
showMpvOsd: (text: string) => void;
|
||||
setTimer: (
|
||||
handler: () => void,
|
||||
timeoutMs: number,
|
||||
) => ReturnType<typeof setTimeout>;
|
||||
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
|
||||
}
|
||||
|
||||
export function createNumericShortcutRuntimeService(
|
||||
options: NumericShortcutRuntimeOptions,
|
||||
) {
|
||||
const createSession = () =>
|
||||
createNumericShortcutSessionService({
|
||||
registerShortcut: (accelerator, handler) =>
|
||||
options.globalShortcut.register(accelerator, handler),
|
||||
unregisterShortcut: (accelerator) =>
|
||||
options.globalShortcut.unregister(accelerator),
|
||||
setTimer: options.setTimer,
|
||||
clearTimer: options.clearTimer,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
});
|
||||
|
||||
return {
|
||||
createSession,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,37 @@
|
||||
interface GlobalShortcutLike {
|
||||
register: (accelerator: string, callback: () => void) => boolean;
|
||||
unregister: (accelerator: string) => void;
|
||||
}
|
||||
|
||||
export interface NumericShortcutRuntimeOptions {
|
||||
globalShortcut: GlobalShortcutLike;
|
||||
showMpvOsd: (text: string) => void;
|
||||
setTimer: (
|
||||
handler: () => void,
|
||||
timeoutMs: number,
|
||||
) => ReturnType<typeof setTimeout>;
|
||||
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
|
||||
}
|
||||
|
||||
export function createNumericShortcutRuntimeService(
|
||||
options: NumericShortcutRuntimeOptions,
|
||||
) {
|
||||
const createSession = () =>
|
||||
createNumericShortcutSessionService({
|
||||
registerShortcut: (accelerator, handler) =>
|
||||
options.globalShortcut.register(accelerator, handler),
|
||||
unregisterShortcut: (accelerator) =>
|
||||
options.globalShortcut.unregister(accelerator),
|
||||
setTimer: options.setTimer,
|
||||
clearTimer: options.clearTimer,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
});
|
||||
|
||||
return {
|
||||
createSession,
|
||||
};
|
||||
}
|
||||
|
||||
export interface NumericShortcutSessionMessages {
|
||||
prompt: string;
|
||||
timeout: string;
|
||||
@@ -1,6 +1,54 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createNumericShortcutSessionService } from "./numeric-shortcut-session-service";
|
||||
import {
|
||||
createNumericShortcutRuntimeService,
|
||||
createNumericShortcutSessionService,
|
||||
} from "./numeric-shortcut-service";
|
||||
|
||||
test("createNumericShortcutRuntimeService creates sessions wired to globalShortcut", () => {
|
||||
const registered: string[] = [];
|
||||
const unregistered: string[] = [];
|
||||
const osd: string[] = [];
|
||||
const handlers = new Map<string, () => void>();
|
||||
|
||||
const runtime = createNumericShortcutRuntimeService({
|
||||
globalShortcut: {
|
||||
register: (accelerator, callback) => {
|
||||
registered.push(accelerator);
|
||||
handlers.set(accelerator, callback);
|
||||
return true;
|
||||
},
|
||||
unregister: (accelerator) => {
|
||||
unregistered.push(accelerator);
|
||||
handlers.delete(accelerator);
|
||||
},
|
||||
},
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
setTimer: () => setTimeout(() => {}, 1000),
|
||||
clearTimer: (timer) => clearTimeout(timer),
|
||||
});
|
||||
|
||||
const session = runtime.createSession();
|
||||
session.start({
|
||||
timeoutMs: 5000,
|
||||
onDigit: () => {},
|
||||
messages: {
|
||||
prompt: "Select count",
|
||||
timeout: "Timed out",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(session.isActive(), true);
|
||||
assert.ok(registered.includes("1"));
|
||||
assert.ok(registered.includes("Escape"));
|
||||
assert.equal(osd[0], "Select count");
|
||||
|
||||
handlers.get("Escape")?.();
|
||||
assert.equal(session.isActive(), false);
|
||||
assert.ok(unregistered.includes("Escape"));
|
||||
});
|
||||
|
||||
test("numeric shortcut session handles digit selection and unregisters shortcuts", () => {
|
||||
const handlers = new Map<string, () => void>();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { KikuFieldGroupingChoice } from "../../types";
|
||||
import {
|
||||
createFieldGroupingCallbackRuntimeService,
|
||||
sendToVisibleOverlayRuntimeService,
|
||||
} from "./overlay-bridge-runtime-service";
|
||||
} from "./overlay-bridge-service";
|
||||
|
||||
test("sendToVisibleOverlayRuntimeService restores visibility flag when opening hidden overlay modal", () => {
|
||||
const sent: unknown[][] = [];
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
KikuFieldGroupingChoice,
|
||||
KikuFieldGroupingRequestData,
|
||||
} from "../../types";
|
||||
import { addOverlayModalRestoreFlagService } from "./overlay-modal-restore-service";
|
||||
import { sendToVisibleOverlayService } from "./overlay-send-service";
|
||||
import { createFieldGroupingCallbackService } from "./field-grouping-service";
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
@@ -16,19 +14,20 @@ export function sendToVisibleOverlayRuntimeService<T extends string>(options: {
|
||||
restoreOnModalClose?: T;
|
||||
restoreVisibleOverlayOnModalClose: Set<T>;
|
||||
}): boolean {
|
||||
return sendToVisibleOverlayService({
|
||||
mainWindow: options.mainWindow,
|
||||
visibleOverlayVisible: options.visibleOverlayVisible,
|
||||
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
|
||||
channel: options.channel,
|
||||
payload: options.payload,
|
||||
restoreOnModalClose: options.restoreOnModalClose,
|
||||
addRestoreFlag: (modal) =>
|
||||
addOverlayModalRestoreFlagService(
|
||||
options.restoreVisibleOverlayOnModalClose,
|
||||
modal as T,
|
||||
),
|
||||
});
|
||||
if (!options.mainWindow || options.mainWindow.isDestroyed()) return false;
|
||||
const wasVisible = options.visibleOverlayVisible;
|
||||
if (!options.visibleOverlayVisible) {
|
||||
options.setVisibleOverlayVisible(true);
|
||||
}
|
||||
if (!wasVisible && options.restoreOnModalClose) {
|
||||
options.restoreVisibleOverlayOnModalClose.add(options.restoreOnModalClose);
|
||||
}
|
||||
if (options.payload === undefined) {
|
||||
options.mainWindow.webContents.send(options.channel);
|
||||
} else {
|
||||
options.mainWindow.webContents.send(options.channel, options.payload);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function createFieldGroupingCallbackRuntimeService<T extends string>(
|
||||
@@ -1,58 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
broadcastToOverlayWindowsRuntimeService,
|
||||
getOverlayWindowsRuntimeService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./overlay-broadcast-runtime-service";
|
||||
|
||||
test("getOverlayWindowsRuntimeService returns non-destroyed windows only", () => {
|
||||
const alive = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
|
||||
const dead = { isDestroyed: () => true } as unknown as Electron.BrowserWindow;
|
||||
const windows = getOverlayWindowsRuntimeService({
|
||||
mainWindow: alive,
|
||||
invisibleWindow: dead,
|
||||
});
|
||||
assert.deepEqual(windows, [alive]);
|
||||
});
|
||||
|
||||
test("broadcastToOverlayWindowsRuntimeService sends channel to each window", () => {
|
||||
const calls: unknown[][] = [];
|
||||
const window = {
|
||||
webContents: {
|
||||
send: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
},
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
broadcastToOverlayWindowsRuntimeService([window], "x", 1, "a");
|
||||
assert.deepEqual(calls, [["x", 1, "a"]]);
|
||||
});
|
||||
|
||||
test("runtime-option and debug broadcasts use expected channels", () => {
|
||||
const broadcasts: unknown[][] = [];
|
||||
broadcastRuntimeOptionsChangedRuntimeService(
|
||||
() => [],
|
||||
(channel, ...args) => {
|
||||
broadcasts.push([channel, ...args]);
|
||||
},
|
||||
);
|
||||
let state = false;
|
||||
const changed = setOverlayDebugVisualizationEnabledRuntimeService(
|
||||
state,
|
||||
true,
|
||||
(enabled) => {
|
||||
state = enabled;
|
||||
},
|
||||
(channel, ...args) => {
|
||||
broadcasts.push([channel, ...args]);
|
||||
},
|
||||
);
|
||||
assert.equal(changed, true);
|
||||
assert.equal(state, true);
|
||||
assert.deepEqual(broadcasts, [
|
||||
["runtime-options:changed", []],
|
||||
["overlay-debug-visualization:set", true],
|
||||
]);
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { RuntimeOptionState } from "../../types";
|
||||
|
||||
export function getOverlayWindowsRuntimeService(options: {
|
||||
mainWindow: BrowserWindow | null;
|
||||
invisibleWindow: BrowserWindow | null;
|
||||
}): BrowserWindow[] {
|
||||
const windows: BrowserWindow[] = [];
|
||||
if (options.mainWindow && !options.mainWindow.isDestroyed()) {
|
||||
windows.push(options.mainWindow);
|
||||
}
|
||||
if (options.invisibleWindow && !options.invisibleWindow.isDestroyed()) {
|
||||
windows.push(options.invisibleWindow);
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
export function broadcastToOverlayWindowsRuntimeService(
|
||||
windows: BrowserWindow[],
|
||||
channel: string,
|
||||
...args: unknown[]
|
||||
): void {
|
||||
for (const window of windows) {
|
||||
window.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function broadcastRuntimeOptionsChangedRuntimeService(
|
||||
getRuntimeOptionsState: () => RuntimeOptionState[],
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||
): void {
|
||||
broadcastToOverlayWindows("runtime-options:changed", getRuntimeOptionsState());
|
||||
}
|
||||
|
||||
export function setOverlayDebugVisualizationEnabledRuntimeService(
|
||||
currentEnabled: boolean,
|
||||
nextEnabled: boolean,
|
||||
setState: (enabled: boolean) => void,
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||
): boolean {
|
||||
if (currentEnabled === nextEnabled) return false;
|
||||
setState(nextEnabled);
|
||||
broadcastToOverlayWindows("overlay-debug-visualization:set", nextEnabled);
|
||||
return true;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createOverlayManagerService } from "./overlay-manager-service";
|
||||
import {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
createOverlayManagerService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./overlay-manager-service";
|
||||
|
||||
test("overlay manager initializes with empty windows and hidden overlays", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
@@ -40,3 +44,55 @@ test("overlay manager stores visibility state", () => {
|
||||
assert.equal(manager.getVisibleOverlayVisible(), true);
|
||||
assert.equal(manager.getInvisibleOverlayVisible(), true);
|
||||
});
|
||||
|
||||
test("overlay manager broadcasts to non-destroyed windows", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const calls: unknown[][] = [];
|
||||
const aliveWindow = {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
send: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
},
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
const deadWindow = {
|
||||
isDestroyed: () => true,
|
||||
webContents: {
|
||||
send: (..._args: unknown[]) => {},
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
|
||||
manager.setMainWindow(aliveWindow);
|
||||
manager.setInvisibleWindow(deadWindow);
|
||||
manager.broadcastToOverlayWindows("x", 1, "a");
|
||||
|
||||
assert.deepEqual(calls, [["x", 1, "a"]]);
|
||||
});
|
||||
|
||||
test("runtime-option and debug broadcasts use expected channels", () => {
|
||||
const broadcasts: unknown[][] = [];
|
||||
broadcastRuntimeOptionsChangedRuntimeService(
|
||||
() => [],
|
||||
(channel, ...args) => {
|
||||
broadcasts.push([channel, ...args]);
|
||||
},
|
||||
);
|
||||
let state = false;
|
||||
const changed = setOverlayDebugVisualizationEnabledRuntimeService(
|
||||
state,
|
||||
true,
|
||||
(enabled) => {
|
||||
state = enabled;
|
||||
},
|
||||
(channel, ...args) => {
|
||||
broadcasts.push([channel, ...args]);
|
||||
},
|
||||
);
|
||||
assert.equal(changed, true);
|
||||
assert.equal(state, true);
|
||||
assert.deepEqual(broadcasts, [
|
||||
["runtime-options:changed", []],
|
||||
["overlay-debug-visualization:set", true],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { RuntimeOptionState } from "../../types";
|
||||
|
||||
export interface OverlayManagerService {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
@@ -10,6 +11,7 @@ export interface OverlayManagerService {
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
export function createOverlayManagerService(): OverlayManagerService {
|
||||
@@ -45,5 +47,36 @@ export function createOverlayManagerService(): OverlayManagerService {
|
||||
}
|
||||
return windows;
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, ...args) => {
|
||||
const windows: BrowserWindow[] = [];
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
windows.push(mainWindow);
|
||||
}
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
windows.push(invisibleWindow);
|
||||
}
|
||||
for (const window of windows) {
|
||||
window.webContents.send(channel, ...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function broadcastRuntimeOptionsChangedRuntimeService(
|
||||
getRuntimeOptionsState: () => RuntimeOptionState[],
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||
): void {
|
||||
broadcastToOverlayWindows("runtime-options:changed", getRuntimeOptionsState());
|
||||
}
|
||||
|
||||
export function setOverlayDebugVisualizationEnabledRuntimeService(
|
||||
currentEnabled: boolean,
|
||||
nextEnabled: boolean,
|
||||
setState: (enabled: boolean) => void,
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||
): boolean {
|
||||
if (currentEnabled === nextEnabled) return false;
|
||||
setState(nextEnabled);
|
||||
broadcastToOverlayWindows("overlay-debug-visualization:set", nextEnabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
addOverlayModalRestoreFlagService,
|
||||
handleOverlayModalClosedService,
|
||||
} from "./overlay-modal-restore-service";
|
||||
|
||||
test("overlay modal restore service adds modal restore flag", () => {
|
||||
const restore = new Set<"runtime-options" | "subsync">();
|
||||
addOverlayModalRestoreFlagService(restore, "runtime-options");
|
||||
assert.equal(restore.has("runtime-options"), true);
|
||||
});
|
||||
|
||||
test("overlay modal restore service hides overlay only when last modal closes", () => {
|
||||
const restore = new Set<"runtime-options" | "subsync">();
|
||||
const visibility: boolean[] = [];
|
||||
|
||||
addOverlayModalRestoreFlagService(restore, "runtime-options");
|
||||
addOverlayModalRestoreFlagService(restore, "subsync");
|
||||
|
||||
handleOverlayModalClosedService(restore, "runtime-options", (visible) => {
|
||||
visibility.push(visible);
|
||||
});
|
||||
assert.equal(visibility.length, 0);
|
||||
|
||||
handleOverlayModalClosedService(restore, "subsync", (visible) => {
|
||||
visibility.push(visible);
|
||||
});
|
||||
assert.deepEqual(visibility, [false]);
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
export function addOverlayModalRestoreFlagService<T extends string>(
|
||||
restoreSet: Set<T>,
|
||||
modal: T,
|
||||
): void {
|
||||
restoreSet.add(modal);
|
||||
}
|
||||
|
||||
export function handleOverlayModalClosedService<T extends string>(
|
||||
restoreSet: Set<T>,
|
||||
modal: T,
|
||||
setVisibleOverlayVisible: (visible: boolean) => void,
|
||||
): void {
|
||||
if (!restoreSet.has(modal)) return;
|
||||
restoreSet.delete(modal);
|
||||
if (restoreSet.size === 0) {
|
||||
setVisibleOverlayVisible(false);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
export function sendToVisibleOverlayService(options: {
|
||||
mainWindow: BrowserWindow | null;
|
||||
visibleOverlayVisible: boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
channel: string;
|
||||
payload?: unknown;
|
||||
restoreOnModalClose?: string;
|
||||
addRestoreFlag: (modal: string) => void;
|
||||
}): boolean {
|
||||
if (!options.mainWindow || options.mainWindow.isDestroyed()) return false;
|
||||
const wasVisible = options.visibleOverlayVisible;
|
||||
if (!options.visibleOverlayVisible) {
|
||||
options.setVisibleOverlayVisible(true);
|
||||
}
|
||||
if (!wasVisible && options.restoreOnModalClose) {
|
||||
options.addRestoreFlag(options.restoreOnModalClose);
|
||||
}
|
||||
if (options.payload === undefined) {
|
||||
options.mainWindow.webContents.send(options.channel);
|
||||
} else {
|
||||
options.mainWindow.webContents.send(options.channel, options.payload);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
|
||||
export interface OverlayShortcutFallbackHandlers {
|
||||
openRuntimeOptions: () => void;
|
||||
openJimaku: () => void;
|
||||
markAudioCard: () => void;
|
||||
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||
copySubtitle: () => void;
|
||||
toggleSecondarySub: () => void;
|
||||
updateLastCardFromClipboard: () => void;
|
||||
triggerFieldGrouping: () => void;
|
||||
triggerSubsync: () => void;
|
||||
mineSentence: () => void;
|
||||
mineSentenceMultiple: (timeoutMs: number) => void;
|
||||
}
|
||||
|
||||
export function runOverlayShortcutLocalFallback(
|
||||
input: Electron.Input,
|
||||
shortcuts: ConfiguredShortcuts,
|
||||
matcher: (
|
||||
input: Electron.Input,
|
||||
accelerator: string,
|
||||
allowWhenRegistered?: boolean,
|
||||
) => boolean,
|
||||
handlers: OverlayShortcutFallbackHandlers,
|
||||
): boolean {
|
||||
const actions: Array<{
|
||||
accelerator: string | null | undefined;
|
||||
run: () => void;
|
||||
allowWhenRegistered?: boolean;
|
||||
}> = [
|
||||
{
|
||||
accelerator: shortcuts.openRuntimeOptions,
|
||||
run: () => {
|
||||
handlers.openRuntimeOptions();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.openJimaku,
|
||||
run: () => {
|
||||
handlers.openJimaku();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.markAudioCard,
|
||||
run: () => {
|
||||
handlers.markAudioCard();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.copySubtitleMultiple,
|
||||
run: () => {
|
||||
handlers.copySubtitleMultiple(shortcuts.multiCopyTimeoutMs);
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.copySubtitle,
|
||||
run: () => {
|
||||
handlers.copySubtitle();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.toggleSecondarySub,
|
||||
run: () => handlers.toggleSecondarySub(),
|
||||
allowWhenRegistered: true,
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.updateLastCardFromClipboard,
|
||||
run: () => {
|
||||
handlers.updateLastCardFromClipboard();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.triggerFieldGrouping,
|
||||
run: () => {
|
||||
handlers.triggerFieldGrouping();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.triggerSubsync,
|
||||
run: () => {
|
||||
handlers.triggerSubsync();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.mineSentence,
|
||||
run: () => {
|
||||
handlers.mineSentence();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.mineSentenceMultiple,
|
||||
run: () => {
|
||||
handlers.mineSentenceMultiple(shortcuts.multiCopyTimeoutMs);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const action of actions) {
|
||||
if (!action.accelerator) continue;
|
||||
if (
|
||||
matcher(
|
||||
input,
|
||||
action.accelerator,
|
||||
action.allowWhenRegistered === true,
|
||||
)
|
||||
) {
|
||||
action.run();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,8 +1,20 @@
|
||||
import {
|
||||
OverlayShortcutFallbackHandlers,
|
||||
} from "./overlay-shortcut-fallback-runner";
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { OverlayShortcutHandlers } from "./overlay-shortcut-service";
|
||||
|
||||
export interface OverlayShortcutFallbackHandlers {
|
||||
openRuntimeOptions: () => void;
|
||||
openJimaku: () => void;
|
||||
markAudioCard: () => void;
|
||||
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||
copySubtitle: () => void;
|
||||
toggleSecondarySub: () => void;
|
||||
updateLastCardFromClipboard: () => void;
|
||||
triggerFieldGrouping: () => void;
|
||||
triggerSubsync: () => void;
|
||||
mineSentence: () => void;
|
||||
mineSentenceMultiple: (timeoutMs: number) => void;
|
||||
}
|
||||
|
||||
export interface OverlayShortcutRuntimeDeps {
|
||||
showMpvOsd: (text: string) => void;
|
||||
openRuntimeOptions: () => void;
|
||||
@@ -103,3 +115,102 @@ export function createOverlayShortcutRuntimeHandlers(
|
||||
|
||||
return { overlayHandlers, fallbackHandlers };
|
||||
}
|
||||
|
||||
export function runOverlayShortcutLocalFallback(
|
||||
input: Electron.Input,
|
||||
shortcuts: ConfiguredShortcuts,
|
||||
matcher: (
|
||||
input: Electron.Input,
|
||||
accelerator: string,
|
||||
allowWhenRegistered?: boolean,
|
||||
) => boolean,
|
||||
handlers: OverlayShortcutFallbackHandlers,
|
||||
): boolean {
|
||||
const actions: Array<{
|
||||
accelerator: string | null | undefined;
|
||||
run: () => void;
|
||||
allowWhenRegistered?: boolean;
|
||||
}> = [
|
||||
{
|
||||
accelerator: shortcuts.openRuntimeOptions,
|
||||
run: () => {
|
||||
handlers.openRuntimeOptions();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.openJimaku,
|
||||
run: () => {
|
||||
handlers.openJimaku();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.markAudioCard,
|
||||
run: () => {
|
||||
handlers.markAudioCard();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.copySubtitleMultiple,
|
||||
run: () => {
|
||||
handlers.copySubtitleMultiple(shortcuts.multiCopyTimeoutMs);
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.copySubtitle,
|
||||
run: () => {
|
||||
handlers.copySubtitle();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.toggleSecondarySub,
|
||||
run: () => handlers.toggleSecondarySub(),
|
||||
allowWhenRegistered: true,
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.updateLastCardFromClipboard,
|
||||
run: () => {
|
||||
handlers.updateLastCardFromClipboard();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.triggerFieldGrouping,
|
||||
run: () => {
|
||||
handlers.triggerFieldGrouping();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.triggerSubsync,
|
||||
run: () => {
|
||||
handlers.triggerSubsync();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.mineSentence,
|
||||
run: () => {
|
||||
handlers.mineSentence();
|
||||
},
|
||||
},
|
||||
{
|
||||
accelerator: shortcuts.mineSentenceMultiple,
|
||||
run: () => {
|
||||
handlers.mineSentenceMultiple(shortcuts.multiCopyTimeoutMs);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const action of actions) {
|
||||
if (!action.accelerator) continue;
|
||||
if (
|
||||
matcher(
|
||||
input,
|
||||
action.accelerator,
|
||||
action.allowWhenRegistered === true,
|
||||
)
|
||||
) {
|
||||
action.run();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { OverlayShortcutHandlers, registerOverlayShortcutsService, unregisterOverlayShortcutsService } from "./overlay-shortcut-service";
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
|
||||
export interface OverlayShortcutLifecycleDeps {
|
||||
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||
getOverlayHandlers: () => OverlayShortcutHandlers;
|
||||
cancelPendingMultiCopy: () => void;
|
||||
cancelPendingMineSentenceMultiple: () => void;
|
||||
}
|
||||
|
||||
export function registerOverlayShortcutsRuntimeService(
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
return registerOverlayShortcutsService(
|
||||
deps.getConfiguredShortcuts(),
|
||||
deps.getOverlayHandlers(),
|
||||
);
|
||||
}
|
||||
|
||||
export function unregisterOverlayShortcutsRuntimeService(
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
if (!shortcutsRegistered) return shortcutsRegistered;
|
||||
deps.cancelPendingMultiCopy();
|
||||
deps.cancelPendingMineSentenceMultiple();
|
||||
unregisterOverlayShortcutsService(deps.getConfiguredShortcuts());
|
||||
return false;
|
||||
}
|
||||
|
||||
export function syncOverlayShortcutsRuntimeService(
|
||||
shouldBeActive: boolean,
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
if (shouldBeActive) {
|
||||
return registerOverlayShortcutsRuntimeService(deps);
|
||||
}
|
||||
return unregisterOverlayShortcutsRuntimeService(shortcutsRegistered, deps);
|
||||
}
|
||||
|
||||
export function refreshOverlayShortcutsRuntimeService(
|
||||
shouldBeActive: boolean,
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
const cleared = unregisterOverlayShortcutsRuntimeService(
|
||||
shortcutsRegistered,
|
||||
deps,
|
||||
);
|
||||
return syncOverlayShortcutsRuntimeService(shouldBeActive, cleared, deps);
|
||||
}
|
||||
@@ -16,6 +16,13 @@ export interface OverlayShortcutHandlers {
|
||||
openJimaku: () => void;
|
||||
}
|
||||
|
||||
export interface OverlayShortcutLifecycleDeps {
|
||||
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||
getOverlayHandlers: () => OverlayShortcutHandlers;
|
||||
cancelPendingMultiCopy: () => void;
|
||||
cancelPendingMineSentenceMultiple: () => void;
|
||||
}
|
||||
|
||||
export function registerOverlayShortcutsService(
|
||||
shortcuts: ConfiguredShortcuts,
|
||||
handlers: OverlayShortcutHandlers,
|
||||
@@ -167,3 +174,46 @@ export function unregisterOverlayShortcutsService(
|
||||
globalShortcut.unregister(shortcuts.openJimaku);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerOverlayShortcutsRuntimeService(
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
return registerOverlayShortcutsService(
|
||||
deps.getConfiguredShortcuts(),
|
||||
deps.getOverlayHandlers(),
|
||||
);
|
||||
}
|
||||
|
||||
export function unregisterOverlayShortcutsRuntimeService(
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
if (!shortcutsRegistered) return shortcutsRegistered;
|
||||
deps.cancelPendingMultiCopy();
|
||||
deps.cancelPendingMineSentenceMultiple();
|
||||
unregisterOverlayShortcutsService(deps.getConfiguredShortcuts());
|
||||
return false;
|
||||
}
|
||||
|
||||
export function syncOverlayShortcutsRuntimeService(
|
||||
shouldBeActive: boolean,
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
if (shouldBeActive) {
|
||||
return registerOverlayShortcutsRuntimeService(deps);
|
||||
}
|
||||
return unregisterOverlayShortcutsRuntimeService(shortcutsRegistered, deps);
|
||||
}
|
||||
|
||||
export function refreshOverlayShortcutsRuntimeService(
|
||||
shouldBeActive: boolean,
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
const cleared = unregisterOverlayShortcutsRuntimeService(
|
||||
shortcutsRegistered,
|
||||
deps,
|
||||
);
|
||||
return syncOverlayShortcutsRuntimeService(shouldBeActive, cleared, deps);
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
export function syncInvisibleOverlayMousePassthroughService(options: {
|
||||
hasInvisibleWindow: () => boolean;
|
||||
setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void;
|
||||
visibleOverlayVisible: boolean;
|
||||
invisibleOverlayVisible: boolean;
|
||||
}): void {
|
||||
if (!options.hasInvisibleWindow()) return;
|
||||
if (options.visibleOverlayVisible) {
|
||||
options.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else if (options.invisibleOverlayVisible) {
|
||||
options.setIgnoreMouseEvents(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function setVisibleOverlayVisibleService(options: {
|
||||
visible: boolean;
|
||||
setVisibleOverlayVisibleState: (visible: boolean) => void;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncInvisibleOverlayMousePassthrough: () => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
isMpvConnected: () => boolean;
|
||||
setMpvSubVisibility: (visible: boolean) => void;
|
||||
}): void {
|
||||
options.setVisibleOverlayVisibleState(options.visible);
|
||||
options.updateVisibleOverlayVisibility();
|
||||
options.updateInvisibleOverlayVisibility();
|
||||
options.syncInvisibleOverlayMousePassthrough();
|
||||
if (
|
||||
options.shouldBindVisibleOverlayToMpvSubVisibility() &&
|
||||
options.isMpvConnected()
|
||||
) {
|
||||
options.setMpvSubVisibility(!options.visible);
|
||||
}
|
||||
}
|
||||
|
||||
export function setInvisibleOverlayVisibleService(options: {
|
||||
visible: boolean;
|
||||
setInvisibleOverlayVisibleState: (visible: boolean) => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncInvisibleOverlayMousePassthrough: () => void;
|
||||
}): void {
|
||||
options.setInvisibleOverlayVisibleState(options.visible);
|
||||
options.updateInvisibleOverlayVisibility();
|
||||
options.syncInvisibleOverlayMousePassthrough();
|
||||
}
|
||||
@@ -24,17 +24,11 @@ export function updateVisibleOverlayVisibilityService(args: {
|
||||
enforceOverlayLayerOrder: () => void;
|
||||
syncOverlayShortcuts: () => void;
|
||||
}): void {
|
||||
console.log(
|
||||
"updateVisibleOverlayVisibility called, visibleOverlayVisible:",
|
||||
args.visibleOverlayVisible,
|
||||
);
|
||||
if (!args.mainWindow || args.mainWindow.isDestroyed()) {
|
||||
console.log("mainWindow not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.visibleOverlayVisible) {
|
||||
console.log("Hiding visible overlay");
|
||||
args.mainWindow.hide();
|
||||
|
||||
if (
|
||||
@@ -57,11 +51,6 @@ export function updateVisibleOverlayVisibilityService(args: {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Should show visible overlay, isTracking:",
|
||||
args.windowTracker?.isTracking(),
|
||||
);
|
||||
|
||||
if (args.shouldBindVisibleOverlayToMpvSubVisibility && args.mpvConnected) {
|
||||
args.mpvSend({
|
||||
command: ["get_property", "secondary-sub-visibility"],
|
||||
@@ -72,11 +61,9 @@ export function updateVisibleOverlayVisibilityService(args: {
|
||||
if (args.windowTracker && args.windowTracker.isTracking()) {
|
||||
args.setTrackerNotReadyWarningShown(false);
|
||||
const geometry = args.windowTracker.getGeometry();
|
||||
console.log("Geometry:", geometry);
|
||||
if (geometry) {
|
||||
args.updateOverlayBounds(geometry);
|
||||
}
|
||||
console.log("Showing visible overlay mainWindow");
|
||||
args.ensureOverlayWindowLevel(args.mainWindow);
|
||||
args.mainWindow.show();
|
||||
args.mainWindow.focus();
|
||||
@@ -96,9 +83,6 @@ export function updateVisibleOverlayVisibilityService(args: {
|
||||
}
|
||||
|
||||
if (!args.trackerNotReadyWarningShown) {
|
||||
console.warn(
|
||||
"Window tracker exists but is not tracking yet; using fallback bounds until tracking starts",
|
||||
);
|
||||
args.setTrackerNotReadyWarningShown(true);
|
||||
}
|
||||
const cursorPoint = screen.getCursorScreenPoint();
|
||||
@@ -181,3 +165,50 @@ export function updateInvisibleOverlayVisibilityService(args: {
|
||||
showInvisibleWithoutFocus();
|
||||
args.syncOverlayShortcuts();
|
||||
}
|
||||
|
||||
export function syncInvisibleOverlayMousePassthroughService(options: {
|
||||
hasInvisibleWindow: () => boolean;
|
||||
setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void;
|
||||
visibleOverlayVisible: boolean;
|
||||
invisibleOverlayVisible: boolean;
|
||||
}): void {
|
||||
if (!options.hasInvisibleWindow()) return;
|
||||
if (options.visibleOverlayVisible) {
|
||||
options.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else if (options.invisibleOverlayVisible) {
|
||||
options.setIgnoreMouseEvents(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function setVisibleOverlayVisibleService(options: {
|
||||
visible: boolean;
|
||||
setVisibleOverlayVisibleState: (visible: boolean) => void;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncInvisibleOverlayMousePassthrough: () => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
isMpvConnected: () => boolean;
|
||||
setMpvSubVisibility: (visible: boolean) => void;
|
||||
}): void {
|
||||
options.setVisibleOverlayVisibleState(options.visible);
|
||||
options.updateVisibleOverlayVisibility();
|
||||
options.updateInvisibleOverlayVisibility();
|
||||
options.syncInvisibleOverlayMousePassthrough();
|
||||
if (
|
||||
options.shouldBindVisibleOverlayToMpvSubVisibility() &&
|
||||
options.isMpvConnected()
|
||||
) {
|
||||
options.setMpvSubVisibility(!options.visible);
|
||||
}
|
||||
}
|
||||
|
||||
export function setInvisibleOverlayVisibleService(options: {
|
||||
visible: boolean;
|
||||
setInvisibleOverlayVisibleState: (visible: boolean) => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncInvisibleOverlayMousePassthrough: () => void;
|
||||
}): void {
|
||||
options.setInvisibleOverlayVisibleState(options.visible);
|
||||
options.updateInvisibleOverlayVisibility();
|
||||
options.syncInvisibleOverlayMousePassthrough();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
applyRuntimeOptionResultRuntimeService,
|
||||
cycleRuntimeOptionFromIpcRuntimeService,
|
||||
setRuntimeOptionFromIpcRuntimeService,
|
||||
} from "./runtime-options-runtime-service";
|
||||
} from "./runtime-options-ipc-service";
|
||||
|
||||
test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
|
||||
const osd: string[] = [];
|
||||
@@ -1,25 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createRuntimeOptionsManagerRuntimeService } from "./runtime-options-manager-runtime-service";
|
||||
|
||||
test("createRuntimeOptionsManagerRuntimeService wires patch + options changed callbacks", () => {
|
||||
const patches: unknown[] = [];
|
||||
const changedSnapshots: unknown[] = [];
|
||||
const manager = createRuntimeOptionsManagerRuntimeService({
|
||||
getAnkiConfig: () => ({
|
||||
behavior: { autoUpdateNewCards: true },
|
||||
isKiku: { fieldGrouping: "manual" },
|
||||
}),
|
||||
applyAnkiPatch: (patch) => {
|
||||
patches.push(patch);
|
||||
},
|
||||
onOptionsChanged: (options) => {
|
||||
changedSnapshots.push(options);
|
||||
},
|
||||
});
|
||||
|
||||
const result = manager.setOptionValue("anki.autoUpdateNewCards", false);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(patches.length > 0, true);
|
||||
assert.equal(changedSnapshots.length > 0, true);
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { RuntimeOptionsManager } from "../../runtime-options";
|
||||
import { AnkiConnectConfig, RuntimeOptionState } from "../../types";
|
||||
|
||||
export interface RuntimeOptionsManagerRuntimeDeps {
|
||||
getAnkiConfig: () => AnkiConnectConfig;
|
||||
applyAnkiPatch: (patch: Partial<AnkiConnectConfig>) => void;
|
||||
onOptionsChanged: (options: RuntimeOptionState[]) => void;
|
||||
}
|
||||
|
||||
export function createRuntimeOptionsManagerRuntimeService(
|
||||
deps: RuntimeOptionsManagerRuntimeDeps,
|
||||
): RuntimeOptionsManager {
|
||||
return new RuntimeOptionsManager(deps.getAnkiConfig, {
|
||||
applyAnkiPatch: deps.applyAnkiPatch,
|
||||
onOptionsChanged: deps.onOptionsChanged,
|
||||
});
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
runOverlayShortcutLocalFallbackRuntimeService,
|
||||
} from "./shortcut-ui-deps-runtime-service";
|
||||
|
||||
function makeOptions() {
|
||||
return {
|
||||
getConfiguredShortcuts: () => ({
|
||||
toggleVisibleOverlayGlobal: null,
|
||||
toggleInvisibleOverlayGlobal: null,
|
||||
copySubtitle: null,
|
||||
copySubtitleMultiple: null,
|
||||
updateLastCardFromClipboard: null,
|
||||
triggerFieldGrouping: null,
|
||||
triggerSubsync: null,
|
||||
mineSentence: null,
|
||||
mineSentenceMultiple: null,
|
||||
multiCopyTimeoutMs: 5000,
|
||||
toggleSecondarySub: null,
|
||||
markAudioCard: null,
|
||||
openRuntimeOptions: "Ctrl+R",
|
||||
openJimaku: null,
|
||||
}),
|
||||
getOverlayShortcutFallbackHandlers: () => ({
|
||||
openRuntimeOptions: () => {},
|
||||
openJimaku: () => {},
|
||||
markAudioCard: () => {},
|
||||
copySubtitleMultiple: () => {},
|
||||
copySubtitle: () => {},
|
||||
toggleSecondarySub: () => {},
|
||||
updateLastCardFromClipboard: () => {},
|
||||
triggerFieldGrouping: () => {},
|
||||
triggerSubsync: () => {},
|
||||
mineSentence: () => {},
|
||||
mineSentenceMultiple: () => {},
|
||||
}),
|
||||
shortcutMatcher: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
test("runOverlayShortcutLocalFallbackRuntimeService delegates and returns boolean", () => {
|
||||
const options = {
|
||||
...makeOptions(),
|
||||
shortcutMatcher: () => true,
|
||||
};
|
||||
|
||||
const handled = runOverlayShortcutLocalFallbackRuntimeService(
|
||||
{
|
||||
key: "r",
|
||||
code: "KeyR",
|
||||
alt: false,
|
||||
control: true,
|
||||
shift: false,
|
||||
meta: false,
|
||||
type: "keyDown",
|
||||
} as Electron.Input,
|
||||
options,
|
||||
);
|
||||
|
||||
assert.equal(handled, true);
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner";
|
||||
|
||||
export interface ShortcutUiRuntimeDepsOptions {
|
||||
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||
getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers;
|
||||
shortcutMatcher: (
|
||||
input: Electron.Input,
|
||||
accelerator: string,
|
||||
allowWhenRegistered?: boolean,
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export function runOverlayShortcutLocalFallbackRuntimeService(
|
||||
input: Electron.Input,
|
||||
options: ShortcutUiRuntimeDepsOptions,
|
||||
): boolean {
|
||||
return runOverlayShortcutLocalFallback(
|
||||
input,
|
||||
options.getConfiguredShortcuts(),
|
||||
options.shortcutMatcher,
|
||||
options.getOverlayShortcutFallbackHandlers(),
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { CliArgs } from "../../cli/args";
|
||||
|
||||
export interface StartupBootstrapRuntimeState {
|
||||
initialArgs: CliArgs;
|
||||
mpvSocketPath: string;
|
||||
texthookerPort: number;
|
||||
backendOverride: string | null;
|
||||
autoStartOverlay: boolean;
|
||||
texthookerOnlyMode: boolean;
|
||||
}
|
||||
|
||||
export interface StartupBootstrapRuntimeDeps {
|
||||
argv: string[];
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
setLogLevelEnv: (level: string) => void;
|
||||
enableVerboseLogging: () => void;
|
||||
forceX11Backend: (args: CliArgs) => void;
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||
getDefaultSocketPath: () => string;
|
||||
defaultTexthookerPort: number;
|
||||
runGenerateConfigFlow: (args: CliArgs) => boolean;
|
||||
startAppLifecycle: (args: CliArgs) => void;
|
||||
}
|
||||
|
||||
export function runStartupBootstrapRuntimeService(
|
||||
deps: StartupBootstrapRuntimeDeps,
|
||||
): StartupBootstrapRuntimeState {
|
||||
const initialArgs = deps.parseArgs(deps.argv);
|
||||
|
||||
if (initialArgs.logLevel) {
|
||||
deps.setLogLevelEnv(initialArgs.logLevel);
|
||||
} else if (initialArgs.verbose) {
|
||||
deps.enableVerboseLogging();
|
||||
}
|
||||
|
||||
deps.forceX11Backend(initialArgs);
|
||||
deps.enforceUnsupportedWaylandMode(initialArgs);
|
||||
|
||||
const state: StartupBootstrapRuntimeState = {
|
||||
initialArgs,
|
||||
mpvSocketPath: initialArgs.socketPath ?? deps.getDefaultSocketPath(),
|
||||
texthookerPort: initialArgs.texthookerPort ?? deps.defaultTexthookerPort,
|
||||
backendOverride: initialArgs.backend ?? null,
|
||||
autoStartOverlay: initialArgs.autoStartOverlay,
|
||||
texthookerOnlyMode: initialArgs.texthooker,
|
||||
};
|
||||
|
||||
if (!deps.runGenerateConfigFlow(initialArgs)) {
|
||||
deps.startAppLifecycle(initialArgs);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
runStartupBootstrapRuntimeService,
|
||||
} from "./startup-bootstrap-runtime-service";
|
||||
} from "./startup-service";
|
||||
import { CliArgs } from "../../cli/args";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
@@ -1,36 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createMecabTokenizerAndCheckRuntimeService,
|
||||
createSubtitleTimingTrackerRuntimeService,
|
||||
} from "./startup-resource-runtime-service";
|
||||
|
||||
test("createMecabTokenizerAndCheckRuntimeService sets tokenizer and checks availability", async () => {
|
||||
const calls: string[] = [];
|
||||
let assigned: unknown = null;
|
||||
await createMecabTokenizerAndCheckRuntimeService({
|
||||
createMecabTokenizer: () => ({
|
||||
checkAvailability: async () => {
|
||||
calls.push("checkAvailability");
|
||||
},
|
||||
}),
|
||||
setMecabTokenizer: (tokenizer) => {
|
||||
assigned = tokenizer;
|
||||
calls.push("setMecabTokenizer");
|
||||
},
|
||||
});
|
||||
assert.equal(assigned !== null, true);
|
||||
assert.deepEqual(calls, ["setMecabTokenizer", "checkAvailability"]);
|
||||
});
|
||||
|
||||
test("createSubtitleTimingTrackerRuntimeService sets created tracker", () => {
|
||||
const tracker = { id: "x" };
|
||||
let assigned: unknown = null;
|
||||
createSubtitleTimingTrackerRuntimeService({
|
||||
createSubtitleTimingTracker: () => tracker,
|
||||
setSubtitleTimingTracker: (value) => {
|
||||
assigned = value;
|
||||
},
|
||||
});
|
||||
assert.equal(assigned, tracker);
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
interface MecabTokenizerLike {
|
||||
checkAvailability: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface SubtitleTimingTrackerLike {}
|
||||
|
||||
export async function createMecabTokenizerAndCheckRuntimeService<
|
||||
T extends MecabTokenizerLike,
|
||||
>(options: {
|
||||
createMecabTokenizer: () => T;
|
||||
setMecabTokenizer: (tokenizer: T) => void;
|
||||
}): Promise<void> {
|
||||
const tokenizer = options.createMecabTokenizer();
|
||||
options.setMecabTokenizer(tokenizer);
|
||||
await tokenizer.checkAvailability();
|
||||
}
|
||||
|
||||
export function createSubtitleTimingTrackerRuntimeService<
|
||||
T extends SubtitleTimingTrackerLike,
|
||||
>(options: {
|
||||
createSubtitleTimingTracker: () => T;
|
||||
setSubtitleTimingTracker: (tracker: T) => void;
|
||||
}): void {
|
||||
const tracker = options.createSubtitleTimingTracker();
|
||||
options.setSubtitleTimingTracker(tracker);
|
||||
}
|
||||
@@ -1,5 +1,58 @@
|
||||
import { CliArgs } from "../../cli/args";
|
||||
import { ConfigValidationWarning, SecondarySubMode } from "../../types";
|
||||
|
||||
export interface StartupBootstrapRuntimeState {
|
||||
initialArgs: CliArgs;
|
||||
mpvSocketPath: string;
|
||||
texthookerPort: number;
|
||||
backendOverride: string | null;
|
||||
autoStartOverlay: boolean;
|
||||
texthookerOnlyMode: boolean;
|
||||
}
|
||||
|
||||
export interface StartupBootstrapRuntimeDeps {
|
||||
argv: string[];
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
setLogLevelEnv: (level: string) => void;
|
||||
enableVerboseLogging: () => void;
|
||||
forceX11Backend: (args: CliArgs) => void;
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||
getDefaultSocketPath: () => string;
|
||||
defaultTexthookerPort: number;
|
||||
runGenerateConfigFlow: (args: CliArgs) => boolean;
|
||||
startAppLifecycle: (args: CliArgs) => void;
|
||||
}
|
||||
|
||||
export function runStartupBootstrapRuntimeService(
|
||||
deps: StartupBootstrapRuntimeDeps,
|
||||
): StartupBootstrapRuntimeState {
|
||||
const initialArgs = deps.parseArgs(deps.argv);
|
||||
|
||||
if (initialArgs.logLevel) {
|
||||
deps.setLogLevelEnv(initialArgs.logLevel);
|
||||
} else if (initialArgs.verbose) {
|
||||
deps.enableVerboseLogging();
|
||||
}
|
||||
|
||||
deps.forceX11Backend(initialArgs);
|
||||
deps.enforceUnsupportedWaylandMode(initialArgs);
|
||||
|
||||
const state: StartupBootstrapRuntimeState = {
|
||||
initialArgs,
|
||||
mpvSocketPath: initialArgs.socketPath ?? deps.getDefaultSocketPath(),
|
||||
texthookerPort: initialArgs.texthookerPort ?? deps.defaultTexthookerPort,
|
||||
backendOverride: initialArgs.backend ?? null,
|
||||
autoStartOverlay: initialArgs.autoStartOverlay,
|
||||
texthookerOnlyMode: initialArgs.texthooker,
|
||||
};
|
||||
|
||||
if (!deps.runGenerateConfigFlow(initialArgs)) {
|
||||
deps.startAppLifecycle(initialArgs);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
interface AppReadyConfigLike {
|
||||
secondarySub?: {
|
||||
defaultMode?: SecondarySubMode;
|
||||
@@ -55,7 +108,10 @@ export async function runAppReadyRuntimeService(
|
||||
const wsEnabled = wsConfig.enabled ?? "auto";
|
||||
const wsPort = wsConfig.port || deps.defaultWebsocketPort;
|
||||
|
||||
if (wsEnabled === true || (wsEnabled === "auto" && !deps.hasMpvWebsocketPlugin())) {
|
||||
if (
|
||||
wsEnabled === true ||
|
||||
(wsEnabled === "auto" && !deps.hasMpvWebsocketPlugin())
|
||||
) {
|
||||
deps.startSubtitleWebsocket(wsPort);
|
||||
} else if (wsEnabled === "auto") {
|
||||
deps.log("mpv_websocket detected, skipping built-in WebSocket server");
|
||||
@@ -1,48 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PartOfSpeech } from "../../types";
|
||||
import { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service";
|
||||
|
||||
test("createTokenizerDepsRuntimeService tokenizes with mecab and merge", async () => {
|
||||
let parserWindow: any = null;
|
||||
let readyPromise: Promise<void> | null = null;
|
||||
let initPromise: Promise<boolean> | null = null;
|
||||
|
||||
const deps = createTokenizerDepsRuntimeService({
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => parserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
parserWindow = window;
|
||||
},
|
||||
getYomitanParserReadyPromise: () => readyPromise,
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
readyPromise = promise;
|
||||
},
|
||||
getYomitanParserInitPromise: () => initPromise,
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
initPromise = promise;
|
||||
},
|
||||
getMecabTokenizer: () => ({
|
||||
tokenize: async () => [
|
||||
{
|
||||
word: "猫",
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
pos1: "名詞",
|
||||
pos2: "一般",
|
||||
pos3: "",
|
||||
pos4: "",
|
||||
inflectionType: "",
|
||||
inflectionForm: "",
|
||||
headword: "猫",
|
||||
katakanaReading: "ネコ",
|
||||
pronunciation: "ネコ",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const merged = await deps.tokenizeWithMecab("猫");
|
||||
assert.ok(Array.isArray(merged));
|
||||
assert.equal(merged?.length, 1);
|
||||
assert.equal(merged?.[0]?.surface, "猫");
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { BrowserWindow, Extension } from "electron";
|
||||
import { mergeTokens } from "../../token-merger";
|
||||
import { TokenizerServiceDeps } from "./tokenizer-service";
|
||||
|
||||
interface RawTokenLike {}
|
||||
|
||||
interface MecabTokenizerLike {
|
||||
tokenize: (text: string) => Promise<RawTokenLike[] | null>;
|
||||
}
|
||||
|
||||
export interface TokenizerDepsRuntimeOptions {
|
||||
getYomitanExt: () => Extension | null;
|
||||
getYomitanParserWindow: () => BrowserWindow | null;
|
||||
setYomitanParserWindow: (window: BrowserWindow | null) => void;
|
||||
getYomitanParserReadyPromise: () => Promise<void> | null;
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||
getYomitanParserInitPromise: () => Promise<boolean> | null;
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
getMecabTokenizer: () => MecabTokenizerLike | null;
|
||||
}
|
||||
|
||||
export function createTokenizerDepsRuntimeService(
|
||||
options: TokenizerDepsRuntimeOptions,
|
||||
): TokenizerServiceDeps {
|
||||
return {
|
||||
getYomitanExt: options.getYomitanExt,
|
||||
getYomitanParserWindow: options.getYomitanParserWindow,
|
||||
setYomitanParserWindow: options.setYomitanParserWindow,
|
||||
getYomitanParserReadyPromise: options.getYomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: options.setYomitanParserReadyPromise,
|
||||
getYomitanParserInitPromise: options.getYomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: options.setYomitanParserInitPromise,
|
||||
tokenizeWithMecab: async (text) => {
|
||||
const mecabTokenizer = options.getMecabTokenizer();
|
||||
if (!mecabTokenizer) {
|
||||
return null;
|
||||
}
|
||||
const rawTokens = await mecabTokenizer.tokenize(text);
|
||||
if (!rawTokens || rawTokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return mergeTokens(rawTokens as never);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BrowserWindow, Extension, session } from "electron";
|
||||
import { MergedToken, PartOfSpeech, SubtitleData } from "../../types";
|
||||
import { mergeTokens } from "../../token-merger";
|
||||
import { MergedToken, PartOfSpeech, SubtitleData, Token } from "../../types";
|
||||
|
||||
interface YomitanParseHeadword {
|
||||
term?: unknown;
|
||||
@@ -28,6 +29,46 @@ export interface TokenizerServiceDeps {
|
||||
tokenizeWithMecab: (text: string) => Promise<MergedToken[] | null>;
|
||||
}
|
||||
|
||||
interface MecabTokenizerLike {
|
||||
tokenize: (text: string) => Promise<Token[] | null>;
|
||||
}
|
||||
|
||||
export interface TokenizerDepsRuntimeOptions {
|
||||
getYomitanExt: () => Extension | null;
|
||||
getYomitanParserWindow: () => BrowserWindow | null;
|
||||
setYomitanParserWindow: (window: BrowserWindow | null) => void;
|
||||
getYomitanParserReadyPromise: () => Promise<void> | null;
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||
getYomitanParserInitPromise: () => Promise<boolean> | null;
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
getMecabTokenizer: () => MecabTokenizerLike | null;
|
||||
}
|
||||
|
||||
export function createTokenizerDepsRuntimeService(
|
||||
options: TokenizerDepsRuntimeOptions,
|
||||
): TokenizerServiceDeps {
|
||||
return {
|
||||
getYomitanExt: options.getYomitanExt,
|
||||
getYomitanParserWindow: options.getYomitanParserWindow,
|
||||
setYomitanParserWindow: options.setYomitanParserWindow,
|
||||
getYomitanParserReadyPromise: options.getYomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: options.setYomitanParserReadyPromise,
|
||||
getYomitanParserInitPromise: options.getYomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: options.setYomitanParserInitPromise,
|
||||
tokenizeWithMecab: async (text) => {
|
||||
const mecabTokenizer = options.getMecabTokenizer();
|
||||
if (!mecabTokenizer) {
|
||||
return null;
|
||||
}
|
||||
const rawTokens = await mecabTokenizer.tokenize(text);
|
||||
if (!rawTokens || rawTokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return mergeTokens(rawTokens);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function extractYomitanHeadword(segment: YomitanParseSegment): string {
|
||||
const headwords = segment.headwords;
|
||||
if (!Array.isArray(headwords) || headwords.length === 0) {
|
||||
|
||||
246
src/main.ts
246
src/main.ts
@@ -94,20 +94,15 @@ import {
|
||||
TexthookerService,
|
||||
applyMpvSubtitleRenderMetricsPatchService,
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
broadcastToOverlayWindowsRuntimeService,
|
||||
copyCurrentSubtitleService,
|
||||
createAppLifecycleDepsRuntimeService,
|
||||
createAppLoggingRuntimeService,
|
||||
createCliCommandDepsRuntimeService,
|
||||
createOverlayManagerService,
|
||||
createFieldGroupingOverlayRuntimeService,
|
||||
createIpcDepsRuntimeService,
|
||||
createMecabTokenizerAndCheckRuntimeService,
|
||||
createNumericShortcutRuntimeService,
|
||||
createOverlayShortcutRuntimeHandlers,
|
||||
createOverlayWindowService,
|
||||
createRuntimeOptionsManagerRuntimeService,
|
||||
createSubtitleTimingTrackerRuntimeService,
|
||||
createTokenizerDepsRuntimeService,
|
||||
cycleSecondarySubModeService,
|
||||
enforceOverlayLayerOrderService,
|
||||
@@ -119,7 +114,6 @@ import {
|
||||
handleMineSentenceDigitService,
|
||||
handleMpvCommandFromIpcService,
|
||||
handleMultiCopyDigitService,
|
||||
handleOverlayModalClosedService,
|
||||
hasMpvWebsocketPlugin,
|
||||
initializeOverlayRuntimeService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
@@ -137,8 +131,6 @@ import {
|
||||
registerOverlayShortcutsService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
resolveJimakuApiKeyService,
|
||||
runGenerateConfigFlowRuntimeService,
|
||||
runOverlayShortcutLocalFallbackRuntimeService,
|
||||
runStartupBootstrapRuntimeService,
|
||||
runSubsyncManualFromIpcRuntimeService,
|
||||
saveSubtitlePositionService,
|
||||
@@ -164,13 +156,13 @@ import {
|
||||
updateOverlayBoundsService,
|
||||
updateVisibleOverlayVisibilityService,
|
||||
} from "./core/services";
|
||||
import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-service";
|
||||
import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runtime-service";
|
||||
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler";
|
||||
import { runAppReadyRuntimeService } from "./core/services/startup-service";
|
||||
import {
|
||||
applyRuntimeOptionResultRuntimeService,
|
||||
cycleRuntimeOptionFromIpcRuntimeService,
|
||||
setRuntimeOptionFromIpcRuntimeService,
|
||||
} from "./core/services/runtime-options-runtime-service";
|
||||
} from "./core/services/runtime-options-ipc-service";
|
||||
import {
|
||||
ConfigService,
|
||||
DEFAULT_CONFIG,
|
||||
@@ -225,7 +217,27 @@ const isDev =
|
||||
process.argv.includes("--dev") || process.argv.includes("--debug");
|
||||
const texthookerService = new TexthookerService();
|
||||
const subtitleWsService = new SubtitleWebSocketService();
|
||||
const appLogger = createAppLoggingRuntimeService();
|
||||
const appLogger = {
|
||||
logInfo: (message: string) => {
|
||||
console.log(message);
|
||||
},
|
||||
logWarning: (message: string) => {
|
||||
console.warn(message);
|
||||
},
|
||||
logNoRunningInstance: () => {
|
||||
console.error("No running instance. Use --start to launch the app.");
|
||||
},
|
||||
logConfigWarning: (warning: {
|
||||
path: string;
|
||||
message: string;
|
||||
value: unknown;
|
||||
fallback: unknown;
|
||||
}) => {
|
||||
console.warn(
|
||||
`[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function getDefaultSocketPath(): string {
|
||||
if (process.platform === "win32") {
|
||||
@@ -292,22 +304,41 @@ let shortcutsRegistered = false;
|
||||
let overlayRuntimeInitialized = false;
|
||||
let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
|
||||
null;
|
||||
let fieldGroupingResolverSequence = 0;
|
||||
let runtimeOptionsManager: RuntimeOptionsManager | null = null;
|
||||
let trackerNotReadyWarningShown = false;
|
||||
let overlayDebugVisualizationEnabled = false;
|
||||
const overlayManager = createOverlayManagerService();
|
||||
type OverlayHostedModal = "runtime-options" | "subsync";
|
||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||
|
||||
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
||||
return fieldGroupingResolver;
|
||||
}
|
||||
|
||||
function setFieldGroupingResolver(
|
||||
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
|
||||
): void {
|
||||
if (!resolver) {
|
||||
fieldGroupingResolver = null;
|
||||
return;
|
||||
}
|
||||
const sequence = ++fieldGroupingResolverSequence;
|
||||
const wrappedResolver = (choice: KikuFieldGroupingChoice): void => {
|
||||
if (sequence !== fieldGroupingResolverSequence) return;
|
||||
resolver(choice);
|
||||
};
|
||||
fieldGroupingResolver = wrappedResolver;
|
||||
}
|
||||
|
||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||
getResolver: () => fieldGroupingResolver,
|
||||
setResolver: (resolver) => {
|
||||
fieldGroupingResolver = resolver;
|
||||
},
|
||||
getResolver: () => getFieldGroupingResolver(),
|
||||
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
||||
});
|
||||
const sendToVisibleOverlay = fieldGroupingOverlayRuntime.sendToVisibleOverlay;
|
||||
@@ -323,7 +354,7 @@ function getOverlayWindows(): BrowserWindow[] {
|
||||
}
|
||||
|
||||
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
||||
broadcastToOverlayWindowsRuntimeService(getOverlayWindows(), channel, ...args);
|
||||
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
||||
}
|
||||
|
||||
function broadcastRuntimeOptionsChanged(): void {
|
||||
@@ -459,25 +490,26 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
},
|
||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
runGenerateConfigFlow: (args) =>
|
||||
runGenerateConfigFlowRuntimeService(args, {
|
||||
shouldStartApp: (nextArgs) => shouldStartApp(nextArgs),
|
||||
generateConfig: async (nextArgs) =>
|
||||
generateDefaultConfigFile(nextArgs, {
|
||||
configDir: CONFIG_DIR,
|
||||
defaultConfig: DEFAULT_CONFIG,
|
||||
generateTemplate: (config) => generateConfigTemplate(config as never),
|
||||
}),
|
||||
onSuccess: (exitCode) => {
|
||||
runGenerateConfigFlow: (args) => {
|
||||
if (!args.generateConfig || shouldStartApp(args)) {
|
||||
return false;
|
||||
}
|
||||
generateDefaultConfigFile(args, {
|
||||
configDir: CONFIG_DIR,
|
||||
defaultConfig: DEFAULT_CONFIG,
|
||||
generateTemplate: (config) => generateConfigTemplate(config as never),
|
||||
})
|
||||
.then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
app.quit();
|
||||
},
|
||||
onError: (error) => {
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error(`Failed to generate config: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
app.quit();
|
||||
},
|
||||
}),
|
||||
});
|
||||
return true;
|
||||
},
|
||||
startAppLifecycle: (args) => {
|
||||
startAppLifecycleService(args, createAppLifecycleDepsRuntimeService({
|
||||
app,
|
||||
@@ -548,18 +580,20 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||
initRuntimeOptionsManager: () => {
|
||||
runtimeOptionsManager = createRuntimeOptionsManagerRuntimeService({
|
||||
getAnkiConfig: () => configService.getConfig().ankiConnect,
|
||||
applyAnkiPatch: (patch) => {
|
||||
if (ankiIntegration) {
|
||||
ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
{
|
||||
applyAnkiPatch: (patch) => {
|
||||
if (ankiIntegration) {
|
||||
ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
onOptionsChanged: () => {
|
||||
broadcastRuntimeOptionsChanged();
|
||||
refreshOverlayShortcuts();
|
||||
},
|
||||
},
|
||||
onOptionsChanged: () => {
|
||||
broadcastRuntimeOptionsChanged();
|
||||
refreshOverlayShortcuts();
|
||||
},
|
||||
});
|
||||
);
|
||||
},
|
||||
setSecondarySubMode: (mode) => {
|
||||
secondarySubMode = mode;
|
||||
@@ -571,20 +605,15 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
subtitleWsService.start(port, () => currentSubText);
|
||||
},
|
||||
log: (message) => appLogger.logInfo(message),
|
||||
createMecabTokenizerAndCheck: async () =>
|
||||
createMecabTokenizerAndCheckRuntimeService({
|
||||
createMecabTokenizer: () => new MecabTokenizer(),
|
||||
setMecabTokenizer: (tokenizer) => {
|
||||
mecabTokenizer = tokenizer;
|
||||
},
|
||||
}),
|
||||
createSubtitleTimingTracker: () =>
|
||||
createSubtitleTimingTrackerRuntimeService({
|
||||
createSubtitleTimingTracker: () => new SubtitleTimingTracker(),
|
||||
setSubtitleTimingTracker: (tracker) => {
|
||||
subtitleTimingTracker = tracker;
|
||||
},
|
||||
}),
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
const tokenizer = new MecabTokenizer();
|
||||
mecabTokenizer = tokenizer;
|
||||
await tokenizer.checkAvailability();
|
||||
},
|
||||
createSubtitleTimingTracker: () => {
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
subtitleTimingTracker = tracker;
|
||||
},
|
||||
loadYomitanExtension: async () => {
|
||||
await loadYomitanExtension();
|
||||
},
|
||||
@@ -596,52 +625,30 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
});
|
||||
},
|
||||
onWillQuitCleanup: () => {
|
||||
runAppShutdownRuntimeService({
|
||||
unregisterAllGlobalShortcuts: () => {
|
||||
globalShortcut.unregisterAll();
|
||||
},
|
||||
stopSubtitleWebsocket: () => {
|
||||
subtitleWsService.stop();
|
||||
},
|
||||
stopTexthookerService: () => {
|
||||
texthookerService.stop();
|
||||
},
|
||||
destroyYomitanParserWindow: () => {
|
||||
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
||||
yomitanParserWindow.destroy();
|
||||
}
|
||||
yomitanParserWindow = null;
|
||||
},
|
||||
clearYomitanParserPromises: () => {
|
||||
yomitanParserReadyPromise = null;
|
||||
yomitanParserInitPromise = null;
|
||||
},
|
||||
stopWindowTracker: () => {
|
||||
if (windowTracker) {
|
||||
windowTracker.stop();
|
||||
}
|
||||
},
|
||||
destroyMpvSocket: () => {
|
||||
if (mpvClient && mpvClient.socket) {
|
||||
mpvClient.socket.destroy();
|
||||
}
|
||||
},
|
||||
clearReconnectTimer: () => {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
}
|
||||
},
|
||||
destroySubtitleTimingTracker: () => {
|
||||
if (subtitleTimingTracker) {
|
||||
subtitleTimingTracker.destroy();
|
||||
}
|
||||
},
|
||||
destroyAnkiIntegration: () => {
|
||||
if (ankiIntegration) {
|
||||
ankiIntegration.destroy();
|
||||
}
|
||||
},
|
||||
});
|
||||
globalShortcut.unregisterAll();
|
||||
subtitleWsService.stop();
|
||||
texthookerService.stop();
|
||||
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
||||
yomitanParserWindow.destroy();
|
||||
}
|
||||
yomitanParserWindow = null;
|
||||
yomitanParserReadyPromise = null;
|
||||
yomitanParserInitPromise = null;
|
||||
if (windowTracker) {
|
||||
windowTracker.stop();
|
||||
}
|
||||
if (mpvClient && mpvClient.socket) {
|
||||
mpvClient.socket.destroy();
|
||||
}
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
}
|
||||
if (subtitleTimingTracker) {
|
||||
subtitleTimingTracker.destroy();
|
||||
}
|
||||
if (ankiIntegration) {
|
||||
ankiIntegration.destroy();
|
||||
}
|
||||
},
|
||||
shouldRestoreWindowsOnActivate: () =>
|
||||
overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,
|
||||
@@ -683,7 +690,9 @@ function handleCliCommand(
|
||||
},
|
||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openInBrowser: (url) => {
|
||||
shell.openExternal(url);
|
||||
void shell.openExternal(url).catch((error) => {
|
||||
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||
});
|
||||
},
|
||||
},
|
||||
overlay: {
|
||||
@@ -898,15 +907,6 @@ function initializeOverlayRuntime(): void {
|
||||
overlayRuntimeInitialized = true;
|
||||
}
|
||||
|
||||
function getShortcutUiRuntimeDeps() {
|
||||
return {
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getOverlayShortcutFallbackHandlers: () =>
|
||||
getOverlayShortcutRuntimeHandlers().fallbackHandlers,
|
||||
shortcutMatcher: shortcutMatchesInputForLocalFallback,
|
||||
};
|
||||
}
|
||||
|
||||
function openYomitanSettings(): void {
|
||||
openYomitanSettingsWindow(
|
||||
{
|
||||
@@ -963,9 +963,11 @@ function getOverlayShortcutRuntimeHandlers() {
|
||||
}
|
||||
|
||||
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
||||
return runOverlayShortcutLocalFallbackRuntimeService(
|
||||
return runOverlayShortcutLocalFallback(
|
||||
input,
|
||||
getShortcutUiRuntimeDeps(),
|
||||
getConfiguredShortcuts(),
|
||||
shortcutMatchesInputForLocalFallback,
|
||||
getOverlayShortcutRuntimeHandlers().fallbackHandlers,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1275,11 +1277,11 @@ function toggleInvisibleOverlay(): void {
|
||||
function setOverlayVisible(visible: boolean): void { setVisibleOverlayVisible(visible); }
|
||||
function toggleOverlay(): void { toggleVisibleOverlay(); }
|
||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||
handleOverlayModalClosedService(
|
||||
restoreVisibleOverlayOnModalClose,
|
||||
modal,
|
||||
(visible) => setVisibleOverlayVisible(visible),
|
||||
);
|
||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||
setVisibleOverlayVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||
@@ -1381,10 +1383,8 @@ registerAnkiJimakuIpcRuntimeService(
|
||||
showDesktopNotification,
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||
getFieldGroupingResolver: () => fieldGroupingResolver,
|
||||
setFieldGroupingResolver: (resolver) => {
|
||||
fieldGroupingResolver = resolver;
|
||||
},
|
||||
getFieldGroupingResolver: () => getFieldGroupingResolver(),
|
||||
setFieldGroupingResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||
parseMediaInfo: (mediaPath) => parseMediaInfo(mediaPath),
|
||||
getCurrentMediaPath: () => currentMediaPath,
|
||||
jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
|
||||
|
||||
Reference in New Issue
Block a user