mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -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;
|
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(
|
export function startAppLifecycleService(
|
||||||
initialArgs: CliArgs,
|
initialArgs: CliArgs,
|
||||||
deps: AppLifecycleServiceDeps,
|
deps: AppLifecycleServiceDeps,
|
||||||
@@ -31,7 +88,11 @@ export function startAppLifecycleService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
deps.onSecondInstance((_event, argv) => {
|
deps.onSecondInstance((_event, argv) => {
|
||||||
|
try {
|
||||||
deps.handleCliCommand(deps.parseArgs(argv), "second-instance");
|
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)) {
|
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 test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
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> = {}) {
|
function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||||
const calls: string[] = [];
|
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;
|
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(
|
function runAsyncWithOsd(
|
||||||
task: () => Promise<void>,
|
task: () => Promise<void>,
|
||||||
deps: CliCommandServiceDeps,
|
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 test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { KikuFieldGroupingChoice } from "../../types";
|
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", () => {
|
test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets restore flag", () => {
|
||||||
const sent: unknown[][] = [];
|
const sent: unknown[][] = [];
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
createFieldGroupingCallbackRuntimeService,
|
createFieldGroupingCallbackRuntimeService,
|
||||||
sendToVisibleOverlayRuntimeService,
|
sendToVisibleOverlayRuntimeService,
|
||||||
} from "./overlay-bridge-runtime-service";
|
} from "./overlay-bridge-service";
|
||||||
|
|
||||||
interface WindowLike {
|
interface WindowLike {
|
||||||
isDestroyed: () => boolean;
|
isDestroyed: () => boolean;
|
||||||
@@ -16,6 +16,16 @@ export function createFieldGroupingCallbackService(options: {
|
|||||||
data: KikuFieldGroupingRequestData,
|
data: KikuFieldGroupingRequestData,
|
||||||
): Promise<KikuFieldGroupingChoice> => {
|
): Promise<KikuFieldGroupingChoice> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
if (options.getResolver()) {
|
||||||
|
resolve({
|
||||||
|
keepNoteId: 0,
|
||||||
|
deleteNoteId: 0,
|
||||||
|
deleteDuplicate: true,
|
||||||
|
cancelled: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const previousVisibleOverlay = options.getVisibleOverlayVisible();
|
const previousVisibleOverlay = options.getVisibleOverlayVisible();
|
||||||
const previousInvisibleOverlay = options.getInvisibleOverlayVisible();
|
const previousInvisibleOverlay = options.getInvisibleOverlayVisible();
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@@ -23,7 +33,9 @@ export function createFieldGroupingCallbackService(options: {
|
|||||||
const finish = (choice: KikuFieldGroupingChoice): void => {
|
const finish = (choice: KikuFieldGroupingChoice): void => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
if (options.getResolver() === finish) {
|
||||||
options.setResolver(null);
|
options.setResolver(null);
|
||||||
|
}
|
||||||
resolve(choice);
|
resolve(choice);
|
||||||
|
|
||||||
if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) {
|
if (!previousVisibleOverlay && options.getVisibleOverlayVisible()) {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
export { TexthookerService } from "./texthooker-service";
|
export { TexthookerService } from "./texthooker-service";
|
||||||
export { hasMpvWebsocketPlugin, SubtitleWebSocketService } from "./subtitle-ws-service";
|
export { hasMpvWebsocketPlugin, SubtitleWebSocketService } from "./subtitle-ws-service";
|
||||||
export { registerGlobalShortcutsService } from "./shortcut-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 { 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 {
|
export {
|
||||||
refreshOverlayShortcutsRuntimeService,
|
refreshOverlayShortcutsRuntimeService,
|
||||||
|
registerOverlayShortcutsService,
|
||||||
syncOverlayShortcutsRuntimeService,
|
syncOverlayShortcutsRuntimeService,
|
||||||
unregisterOverlayShortcutsRuntimeService,
|
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 {
|
export {
|
||||||
copyCurrentSubtitleService,
|
copyCurrentSubtitleService,
|
||||||
handleMineSentenceDigitService,
|
handleMineSentenceDigitService,
|
||||||
@@ -20,15 +20,15 @@ export {
|
|||||||
mineSentenceCardService,
|
mineSentenceCardService,
|
||||||
triggerFieldGroupingService,
|
triggerFieldGroupingService,
|
||||||
updateLastCardFromClipboardService,
|
updateLastCardFromClipboardService,
|
||||||
} from "./mining-runtime-service";
|
} from "./mining-service";
|
||||||
export { startAppLifecycleService } from "./app-lifecycle-service";
|
export { createAppLifecycleDepsRuntimeService, startAppLifecycleService } from "./app-lifecycle-service";
|
||||||
export {
|
export {
|
||||||
playNextSubtitleRuntimeService,
|
playNextSubtitleRuntimeService,
|
||||||
replayCurrentSubtitleRuntimeService,
|
replayCurrentSubtitleRuntimeService,
|
||||||
sendMpvCommandRuntimeService,
|
sendMpvCommandRuntimeService,
|
||||||
setMpvSubVisibilityRuntimeService,
|
setMpvSubVisibilityRuntimeService,
|
||||||
showMpvOsdRuntimeService,
|
showMpvOsdRuntimeService,
|
||||||
} from "./mpv-runtime-service";
|
} from "./mpv-control-service";
|
||||||
export {
|
export {
|
||||||
getInitialInvisibleOverlayVisibilityService,
|
getInitialInvisibleOverlayVisibilityService,
|
||||||
isAutoUpdateEnabledRuntimeService,
|
isAutoUpdateEnabledRuntimeService,
|
||||||
@@ -36,14 +36,14 @@ export {
|
|||||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||||
} from "./runtime-config-service";
|
} from "./runtime-config-service";
|
||||||
export { openYomitanSettingsWindow } from "./yomitan-settings-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 { loadYomitanExtensionService } from "./yomitan-extension-loader-service";
|
||||||
export {
|
export {
|
||||||
getJimakuLanguagePreferenceService,
|
getJimakuLanguagePreferenceService,
|
||||||
getJimakuMaxEntryResultsService,
|
getJimakuMaxEntryResultsService,
|
||||||
jimakuFetchJsonService,
|
jimakuFetchJsonService,
|
||||||
resolveJimakuApiKeyService,
|
resolveJimakuApiKeyService,
|
||||||
} from "./jimaku-runtime-service";
|
} from "./jimaku-service";
|
||||||
export {
|
export {
|
||||||
loadSubtitlePositionService,
|
loadSubtitlePositionService,
|
||||||
saveSubtitlePositionService,
|
saveSubtitlePositionService,
|
||||||
@@ -60,33 +60,19 @@ export {
|
|||||||
setInvisibleOverlayVisibleService,
|
setInvisibleOverlayVisibleService,
|
||||||
setVisibleOverlayVisibleService,
|
setVisibleOverlayVisibleService,
|
||||||
syncInvisibleOverlayMousePassthroughService,
|
syncInvisibleOverlayMousePassthroughService,
|
||||||
} from "./overlay-visibility-runtime-service";
|
updateInvisibleOverlayVisibilityService,
|
||||||
|
updateVisibleOverlayVisibilityService,
|
||||||
|
} from "./overlay-visibility-service";
|
||||||
export { MpvIpcClient, MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from "./mpv-service";
|
export { MpvIpcClient, MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from "./mpv-service";
|
||||||
export { applyMpvSubtitleRenderMetricsPatchService } from "./mpv-render-metrics-service";
|
export { applyMpvSubtitleRenderMetricsPatchService } from "./mpv-render-metrics-service";
|
||||||
export { handleMpvCommandFromIpcService } from "./ipc-command-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 {
|
export {
|
||||||
broadcastRuntimeOptionsChangedRuntimeService,
|
broadcastRuntimeOptionsChangedRuntimeService,
|
||||||
broadcastToOverlayWindowsRuntimeService,
|
createOverlayManagerService,
|
||||||
getOverlayWindowsRuntimeService,
|
|
||||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||||
} from "./overlay-broadcast-runtime-service";
|
} from "./overlay-manager-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";
|
|
||||||
|
|||||||
@@ -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";
|
import { BrowserWindow, ipcMain, IpcMainEvent } from "electron";
|
||||||
|
|
||||||
export interface IpcServiceDeps {
|
export interface IpcServiceDeps {
|
||||||
getInvisibleWindow: () => BrowserWindow | null;
|
getInvisibleWindow: () => WindowLike | null;
|
||||||
isVisibleOverlayVisible: () => boolean;
|
isVisibleOverlayVisible: () => boolean;
|
||||||
setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
setInvisibleIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
||||||
onOverlayModalClosed: (modal: string) => void;
|
onOverlayModalClosed: (modal: string) => void;
|
||||||
@@ -30,6 +30,105 @@ export interface IpcServiceDeps {
|
|||||||
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
|
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 {
|
export function registerIpcHandlersService(deps: IpcServiceDeps): void {
|
||||||
ipcMain.on(
|
ipcMain.on(
|
||||||
"set-ignore-mouse-events",
|
"set-ignore-mouse-events",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
sendMpvCommandRuntimeService,
|
sendMpvCommandRuntimeService,
|
||||||
setMpvSubVisibilityRuntimeService,
|
setMpvSubVisibilityRuntimeService,
|
||||||
showMpvOsdRuntimeService,
|
showMpvOsdRuntimeService,
|
||||||
} from "./mpv-runtime-service";
|
} from "./mpv-control-service";
|
||||||
|
|
||||||
test("showMpvOsdRuntimeService sends show-text when connected", () => {
|
test("showMpvOsdRuntimeService sends show-text when connected", () => {
|
||||||
const commands: (string | number)[][] = [];
|
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 {
|
export interface NumericShortcutSessionMessages {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
timeout: string;
|
timeout: string;
|
||||||
@@ -1,6 +1,54 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
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", () => {
|
test("numeric shortcut session handles digit selection and unregisters shortcuts", () => {
|
||||||
const handlers = new Map<string, () => void>();
|
const handlers = new Map<string, () => void>();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { KikuFieldGroupingChoice } from "../../types";
|
|||||||
import {
|
import {
|
||||||
createFieldGroupingCallbackRuntimeService,
|
createFieldGroupingCallbackRuntimeService,
|
||||||
sendToVisibleOverlayRuntimeService,
|
sendToVisibleOverlayRuntimeService,
|
||||||
} from "./overlay-bridge-runtime-service";
|
} from "./overlay-bridge-service";
|
||||||
|
|
||||||
test("sendToVisibleOverlayRuntimeService restores visibility flag when opening hidden overlay modal", () => {
|
test("sendToVisibleOverlayRuntimeService restores visibility flag when opening hidden overlay modal", () => {
|
||||||
const sent: unknown[][] = [];
|
const sent: unknown[][] = [];
|
||||||
@@ -2,8 +2,6 @@ import {
|
|||||||
KikuFieldGroupingChoice,
|
KikuFieldGroupingChoice,
|
||||||
KikuFieldGroupingRequestData,
|
KikuFieldGroupingRequestData,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import { addOverlayModalRestoreFlagService } from "./overlay-modal-restore-service";
|
|
||||||
import { sendToVisibleOverlayService } from "./overlay-send-service";
|
|
||||||
import { createFieldGroupingCallbackService } from "./field-grouping-service";
|
import { createFieldGroupingCallbackService } from "./field-grouping-service";
|
||||||
import { BrowserWindow } from "electron";
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
@@ -16,19 +14,20 @@ export function sendToVisibleOverlayRuntimeService<T extends string>(options: {
|
|||||||
restoreOnModalClose?: T;
|
restoreOnModalClose?: T;
|
||||||
restoreVisibleOverlayOnModalClose: Set<T>;
|
restoreVisibleOverlayOnModalClose: Set<T>;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
return sendToVisibleOverlayService({
|
if (!options.mainWindow || options.mainWindow.isDestroyed()) return false;
|
||||||
mainWindow: options.mainWindow,
|
const wasVisible = options.visibleOverlayVisible;
|
||||||
visibleOverlayVisible: options.visibleOverlayVisible,
|
if (!options.visibleOverlayVisible) {
|
||||||
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
|
options.setVisibleOverlayVisible(true);
|
||||||
channel: options.channel,
|
}
|
||||||
payload: options.payload,
|
if (!wasVisible && options.restoreOnModalClose) {
|
||||||
restoreOnModalClose: options.restoreOnModalClose,
|
options.restoreVisibleOverlayOnModalClose.add(options.restoreOnModalClose);
|
||||||
addRestoreFlag: (modal) =>
|
}
|
||||||
addOverlayModalRestoreFlagService(
|
if (options.payload === undefined) {
|
||||||
options.restoreVisibleOverlayOnModalClose,
|
options.mainWindow.webContents.send(options.channel);
|
||||||
modal as T,
|
} else {
|
||||||
),
|
options.mainWindow.webContents.send(options.channel, options.payload);
|
||||||
});
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFieldGroupingCallbackRuntimeService<T extends string>(
|
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 test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
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", () => {
|
test("overlay manager initializes with empty windows and hidden overlays", () => {
|
||||||
const manager = createOverlayManagerService();
|
const manager = createOverlayManagerService();
|
||||||
@@ -40,3 +44,55 @@ test("overlay manager stores visibility state", () => {
|
|||||||
assert.equal(manager.getVisibleOverlayVisible(), true);
|
assert.equal(manager.getVisibleOverlayVisible(), true);
|
||||||
assert.equal(manager.getInvisibleOverlayVisible(), 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 { BrowserWindow } from "electron";
|
||||||
|
import { RuntimeOptionState } from "../../types";
|
||||||
|
|
||||||
export interface OverlayManagerService {
|
export interface OverlayManagerService {
|
||||||
getMainWindow: () => BrowserWindow | null;
|
getMainWindow: () => BrowserWindow | null;
|
||||||
@@ -10,6 +11,7 @@ export interface OverlayManagerService {
|
|||||||
getInvisibleOverlayVisible: () => boolean;
|
getInvisibleOverlayVisible: () => boolean;
|
||||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||||
getOverlayWindows: () => BrowserWindow[];
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
|
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOverlayManagerService(): OverlayManagerService {
|
export function createOverlayManagerService(): OverlayManagerService {
|
||||||
@@ -45,5 +47,36 @@ export function createOverlayManagerService(): OverlayManagerService {
|
|||||||
}
|
}
|
||||||
return windows;
|
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 {
|
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||||
OverlayShortcutFallbackHandlers,
|
|
||||||
} from "./overlay-shortcut-fallback-runner";
|
|
||||||
import { OverlayShortcutHandlers } from "./overlay-shortcut-service";
|
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 {
|
export interface OverlayShortcutRuntimeDeps {
|
||||||
showMpvOsd: (text: string) => void;
|
showMpvOsd: (text: string) => void;
|
||||||
openRuntimeOptions: () => void;
|
openRuntimeOptions: () => void;
|
||||||
@@ -103,3 +115,102 @@ export function createOverlayShortcutRuntimeHandlers(
|
|||||||
|
|
||||||
return { overlayHandlers, fallbackHandlers };
|
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;
|
openJimaku: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OverlayShortcutLifecycleDeps {
|
||||||
|
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||||
|
getOverlayHandlers: () => OverlayShortcutHandlers;
|
||||||
|
cancelPendingMultiCopy: () => void;
|
||||||
|
cancelPendingMineSentenceMultiple: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function registerOverlayShortcutsService(
|
export function registerOverlayShortcutsService(
|
||||||
shortcuts: ConfiguredShortcuts,
|
shortcuts: ConfiguredShortcuts,
|
||||||
handlers: OverlayShortcutHandlers,
|
handlers: OverlayShortcutHandlers,
|
||||||
@@ -167,3 +174,46 @@ export function unregisterOverlayShortcutsService(
|
|||||||
globalShortcut.unregister(shortcuts.openJimaku);
|
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;
|
enforceOverlayLayerOrder: () => void;
|
||||||
syncOverlayShortcuts: () => void;
|
syncOverlayShortcuts: () => void;
|
||||||
}): void {
|
}): void {
|
||||||
console.log(
|
|
||||||
"updateVisibleOverlayVisibility called, visibleOverlayVisible:",
|
|
||||||
args.visibleOverlayVisible,
|
|
||||||
);
|
|
||||||
if (!args.mainWindow || args.mainWindow.isDestroyed()) {
|
if (!args.mainWindow || args.mainWindow.isDestroyed()) {
|
||||||
console.log("mainWindow not available");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.visibleOverlayVisible) {
|
if (!args.visibleOverlayVisible) {
|
||||||
console.log("Hiding visible overlay");
|
|
||||||
args.mainWindow.hide();
|
args.mainWindow.hide();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -57,11 +51,6 @@ export function updateVisibleOverlayVisibilityService(args: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Should show visible overlay, isTracking:",
|
|
||||||
args.windowTracker?.isTracking(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (args.shouldBindVisibleOverlayToMpvSubVisibility && args.mpvConnected) {
|
if (args.shouldBindVisibleOverlayToMpvSubVisibility && args.mpvConnected) {
|
||||||
args.mpvSend({
|
args.mpvSend({
|
||||||
command: ["get_property", "secondary-sub-visibility"],
|
command: ["get_property", "secondary-sub-visibility"],
|
||||||
@@ -72,11 +61,9 @@ export function updateVisibleOverlayVisibilityService(args: {
|
|||||||
if (args.windowTracker && args.windowTracker.isTracking()) {
|
if (args.windowTracker && args.windowTracker.isTracking()) {
|
||||||
args.setTrackerNotReadyWarningShown(false);
|
args.setTrackerNotReadyWarningShown(false);
|
||||||
const geometry = args.windowTracker.getGeometry();
|
const geometry = args.windowTracker.getGeometry();
|
||||||
console.log("Geometry:", geometry);
|
|
||||||
if (geometry) {
|
if (geometry) {
|
||||||
args.updateOverlayBounds(geometry);
|
args.updateOverlayBounds(geometry);
|
||||||
}
|
}
|
||||||
console.log("Showing visible overlay mainWindow");
|
|
||||||
args.ensureOverlayWindowLevel(args.mainWindow);
|
args.ensureOverlayWindowLevel(args.mainWindow);
|
||||||
args.mainWindow.show();
|
args.mainWindow.show();
|
||||||
args.mainWindow.focus();
|
args.mainWindow.focus();
|
||||||
@@ -96,9 +83,6 @@ export function updateVisibleOverlayVisibilityService(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!args.trackerNotReadyWarningShown) {
|
if (!args.trackerNotReadyWarningShown) {
|
||||||
console.warn(
|
|
||||||
"Window tracker exists but is not tracking yet; using fallback bounds until tracking starts",
|
|
||||||
);
|
|
||||||
args.setTrackerNotReadyWarningShown(true);
|
args.setTrackerNotReadyWarningShown(true);
|
||||||
}
|
}
|
||||||
const cursorPoint = screen.getCursorScreenPoint();
|
const cursorPoint = screen.getCursorScreenPoint();
|
||||||
@@ -181,3 +165,50 @@ export function updateInvisibleOverlayVisibilityService(args: {
|
|||||||
showInvisibleWithoutFocus();
|
showInvisibleWithoutFocus();
|
||||||
args.syncOverlayShortcuts();
|
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,
|
applyRuntimeOptionResultRuntimeService,
|
||||||
cycleRuntimeOptionFromIpcRuntimeService,
|
cycleRuntimeOptionFromIpcRuntimeService,
|
||||||
setRuntimeOptionFromIpcRuntimeService,
|
setRuntimeOptionFromIpcRuntimeService,
|
||||||
} from "./runtime-options-runtime-service";
|
} from "./runtime-options-ipc-service";
|
||||||
|
|
||||||
test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
|
test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
|
||||||
const osd: string[] = [];
|
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 assert from "node:assert/strict";
|
||||||
import {
|
import {
|
||||||
runStartupBootstrapRuntimeService,
|
runStartupBootstrapRuntimeService,
|
||||||
} from "./startup-bootstrap-runtime-service";
|
} from "./startup-service";
|
||||||
import { CliArgs } from "../../cli/args";
|
import { CliArgs } from "../../cli/args";
|
||||||
|
|
||||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
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";
|
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 {
|
interface AppReadyConfigLike {
|
||||||
secondarySub?: {
|
secondarySub?: {
|
||||||
defaultMode?: SecondarySubMode;
|
defaultMode?: SecondarySubMode;
|
||||||
@@ -55,7 +108,10 @@ export async function runAppReadyRuntimeService(
|
|||||||
const wsEnabled = wsConfig.enabled ?? "auto";
|
const wsEnabled = wsConfig.enabled ?? "auto";
|
||||||
const wsPort = wsConfig.port || deps.defaultWebsocketPort;
|
const wsPort = wsConfig.port || deps.defaultWebsocketPort;
|
||||||
|
|
||||||
if (wsEnabled === true || (wsEnabled === "auto" && !deps.hasMpvWebsocketPlugin())) {
|
if (
|
||||||
|
wsEnabled === true ||
|
||||||
|
(wsEnabled === "auto" && !deps.hasMpvWebsocketPlugin())
|
||||||
|
) {
|
||||||
deps.startSubtitleWebsocket(wsPort);
|
deps.startSubtitleWebsocket(wsPort);
|
||||||
} else if (wsEnabled === "auto") {
|
} else if (wsEnabled === "auto") {
|
||||||
deps.log("mpv_websocket detected, skipping built-in WebSocket server");
|
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 { 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 {
|
interface YomitanParseHeadword {
|
||||||
term?: unknown;
|
term?: unknown;
|
||||||
@@ -28,6 +29,46 @@ export interface TokenizerServiceDeps {
|
|||||||
tokenizeWithMecab: (text: string) => Promise<MergedToken[] | null>;
|
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 {
|
function extractYomitanHeadword(segment: YomitanParseSegment): string {
|
||||||
const headwords = segment.headwords;
|
const headwords = segment.headwords;
|
||||||
if (!Array.isArray(headwords) || headwords.length === 0) {
|
if (!Array.isArray(headwords) || headwords.length === 0) {
|
||||||
|
|||||||
166
src/main.ts
166
src/main.ts
@@ -94,20 +94,15 @@ import {
|
|||||||
TexthookerService,
|
TexthookerService,
|
||||||
applyMpvSubtitleRenderMetricsPatchService,
|
applyMpvSubtitleRenderMetricsPatchService,
|
||||||
broadcastRuntimeOptionsChangedRuntimeService,
|
broadcastRuntimeOptionsChangedRuntimeService,
|
||||||
broadcastToOverlayWindowsRuntimeService,
|
|
||||||
copyCurrentSubtitleService,
|
copyCurrentSubtitleService,
|
||||||
createAppLifecycleDepsRuntimeService,
|
createAppLifecycleDepsRuntimeService,
|
||||||
createAppLoggingRuntimeService,
|
|
||||||
createCliCommandDepsRuntimeService,
|
createCliCommandDepsRuntimeService,
|
||||||
createOverlayManagerService,
|
createOverlayManagerService,
|
||||||
createFieldGroupingOverlayRuntimeService,
|
createFieldGroupingOverlayRuntimeService,
|
||||||
createIpcDepsRuntimeService,
|
createIpcDepsRuntimeService,
|
||||||
createMecabTokenizerAndCheckRuntimeService,
|
|
||||||
createNumericShortcutRuntimeService,
|
createNumericShortcutRuntimeService,
|
||||||
createOverlayShortcutRuntimeHandlers,
|
createOverlayShortcutRuntimeHandlers,
|
||||||
createOverlayWindowService,
|
createOverlayWindowService,
|
||||||
createRuntimeOptionsManagerRuntimeService,
|
|
||||||
createSubtitleTimingTrackerRuntimeService,
|
|
||||||
createTokenizerDepsRuntimeService,
|
createTokenizerDepsRuntimeService,
|
||||||
cycleSecondarySubModeService,
|
cycleSecondarySubModeService,
|
||||||
enforceOverlayLayerOrderService,
|
enforceOverlayLayerOrderService,
|
||||||
@@ -119,7 +114,6 @@ import {
|
|||||||
handleMineSentenceDigitService,
|
handleMineSentenceDigitService,
|
||||||
handleMpvCommandFromIpcService,
|
handleMpvCommandFromIpcService,
|
||||||
handleMultiCopyDigitService,
|
handleMultiCopyDigitService,
|
||||||
handleOverlayModalClosedService,
|
|
||||||
hasMpvWebsocketPlugin,
|
hasMpvWebsocketPlugin,
|
||||||
initializeOverlayRuntimeService,
|
initializeOverlayRuntimeService,
|
||||||
isAutoUpdateEnabledRuntimeService,
|
isAutoUpdateEnabledRuntimeService,
|
||||||
@@ -137,8 +131,6 @@ import {
|
|||||||
registerOverlayShortcutsService,
|
registerOverlayShortcutsService,
|
||||||
replayCurrentSubtitleRuntimeService,
|
replayCurrentSubtitleRuntimeService,
|
||||||
resolveJimakuApiKeyService,
|
resolveJimakuApiKeyService,
|
||||||
runGenerateConfigFlowRuntimeService,
|
|
||||||
runOverlayShortcutLocalFallbackRuntimeService,
|
|
||||||
runStartupBootstrapRuntimeService,
|
runStartupBootstrapRuntimeService,
|
||||||
runSubsyncManualFromIpcRuntimeService,
|
runSubsyncManualFromIpcRuntimeService,
|
||||||
saveSubtitlePositionService,
|
saveSubtitlePositionService,
|
||||||
@@ -164,13 +156,13 @@ import {
|
|||||||
updateOverlayBoundsService,
|
updateOverlayBoundsService,
|
||||||
updateVisibleOverlayVisibilityService,
|
updateVisibleOverlayVisibilityService,
|
||||||
} from "./core/services";
|
} from "./core/services";
|
||||||
import { runAppReadyRuntimeService } from "./core/services/app-ready-runtime-service";
|
import { runOverlayShortcutLocalFallback } from "./core/services/overlay-shortcut-handler";
|
||||||
import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runtime-service";
|
import { runAppReadyRuntimeService } from "./core/services/startup-service";
|
||||||
import {
|
import {
|
||||||
applyRuntimeOptionResultRuntimeService,
|
applyRuntimeOptionResultRuntimeService,
|
||||||
cycleRuntimeOptionFromIpcRuntimeService,
|
cycleRuntimeOptionFromIpcRuntimeService,
|
||||||
setRuntimeOptionFromIpcRuntimeService,
|
setRuntimeOptionFromIpcRuntimeService,
|
||||||
} from "./core/services/runtime-options-runtime-service";
|
} from "./core/services/runtime-options-ipc-service";
|
||||||
import {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
@@ -225,7 +217,27 @@ const isDev =
|
|||||||
process.argv.includes("--dev") || process.argv.includes("--debug");
|
process.argv.includes("--dev") || process.argv.includes("--debug");
|
||||||
const texthookerService = new TexthookerService();
|
const texthookerService = new TexthookerService();
|
||||||
const subtitleWsService = new SubtitleWebSocketService();
|
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 {
|
function getDefaultSocketPath(): string {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
@@ -292,22 +304,41 @@ let shortcutsRegistered = false;
|
|||||||
let overlayRuntimeInitialized = false;
|
let overlayRuntimeInitialized = false;
|
||||||
let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
|
let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
|
||||||
null;
|
null;
|
||||||
|
let fieldGroupingResolverSequence = 0;
|
||||||
let runtimeOptionsManager: RuntimeOptionsManager | null = null;
|
let runtimeOptionsManager: RuntimeOptionsManager | null = null;
|
||||||
let trackerNotReadyWarningShown = false;
|
let trackerNotReadyWarningShown = false;
|
||||||
let overlayDebugVisualizationEnabled = false;
|
let overlayDebugVisualizationEnabled = false;
|
||||||
const overlayManager = createOverlayManagerService();
|
const overlayManager = createOverlayManagerService();
|
||||||
type OverlayHostedModal = "runtime-options" | "subsync";
|
type OverlayHostedModal = "runtime-options" | "subsync";
|
||||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
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>({
|
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||||
getResolver: () => fieldGroupingResolver,
|
getResolver: () => getFieldGroupingResolver(),
|
||||||
setResolver: (resolver) => {
|
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||||
fieldGroupingResolver = resolver;
|
|
||||||
},
|
|
||||||
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
||||||
});
|
});
|
||||||
const sendToVisibleOverlay = fieldGroupingOverlayRuntime.sendToVisibleOverlay;
|
const sendToVisibleOverlay = fieldGroupingOverlayRuntime.sendToVisibleOverlay;
|
||||||
@@ -323,7 +354,7 @@ function getOverlayWindows(): BrowserWindow[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
||||||
broadcastToOverlayWindowsRuntimeService(getOverlayWindows(), channel, ...args);
|
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastRuntimeOptionsChanged(): void {
|
function broadcastRuntimeOptionsChanged(): void {
|
||||||
@@ -459,25 +490,26 @@ const startupState = runStartupBootstrapRuntimeService({
|
|||||||
},
|
},
|
||||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||||
runGenerateConfigFlow: (args) =>
|
runGenerateConfigFlow: (args) => {
|
||||||
runGenerateConfigFlowRuntimeService(args, {
|
if (!args.generateConfig || shouldStartApp(args)) {
|
||||||
shouldStartApp: (nextArgs) => shouldStartApp(nextArgs),
|
return false;
|
||||||
generateConfig: async (nextArgs) =>
|
}
|
||||||
generateDefaultConfigFile(nextArgs, {
|
generateDefaultConfigFile(args, {
|
||||||
configDir: CONFIG_DIR,
|
configDir: CONFIG_DIR,
|
||||||
defaultConfig: DEFAULT_CONFIG,
|
defaultConfig: DEFAULT_CONFIG,
|
||||||
generateTemplate: (config) => generateConfigTemplate(config as never),
|
generateTemplate: (config) => generateConfigTemplate(config as never),
|
||||||
}),
|
})
|
||||||
onSuccess: (exitCode) => {
|
.then((exitCode) => {
|
||||||
process.exitCode = exitCode;
|
process.exitCode = exitCode;
|
||||||
app.quit();
|
app.quit();
|
||||||
},
|
})
|
||||||
onError: (error) => {
|
.catch((error: Error) => {
|
||||||
console.error(`Failed to generate config: ${error.message}`);
|
console.error(`Failed to generate config: ${error.message}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
app.quit();
|
app.quit();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
startAppLifecycle: (args) => {
|
startAppLifecycle: (args) => {
|
||||||
startAppLifecycleService(args, createAppLifecycleDepsRuntimeService({
|
startAppLifecycleService(args, createAppLifecycleDepsRuntimeService({
|
||||||
app,
|
app,
|
||||||
@@ -548,8 +580,9 @@ const startupState = runStartupBootstrapRuntimeService({
|
|||||||
getConfigWarnings: () => configService.getWarnings(),
|
getConfigWarnings: () => configService.getWarnings(),
|
||||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||||
initRuntimeOptionsManager: () => {
|
initRuntimeOptionsManager: () => {
|
||||||
runtimeOptionsManager = createRuntimeOptionsManagerRuntimeService({
|
runtimeOptionsManager = new RuntimeOptionsManager(
|
||||||
getAnkiConfig: () => configService.getConfig().ankiConnect,
|
() => configService.getConfig().ankiConnect,
|
||||||
|
{
|
||||||
applyAnkiPatch: (patch) => {
|
applyAnkiPatch: (patch) => {
|
||||||
if (ankiIntegration) {
|
if (ankiIntegration) {
|
||||||
ankiIntegration.applyRuntimeConfigPatch(patch);
|
ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||||
@@ -559,7 +592,8 @@ const startupState = runStartupBootstrapRuntimeService({
|
|||||||
broadcastRuntimeOptionsChanged();
|
broadcastRuntimeOptionsChanged();
|
||||||
refreshOverlayShortcuts();
|
refreshOverlayShortcuts();
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
setSecondarySubMode: (mode) => {
|
setSecondarySubMode: (mode) => {
|
||||||
secondarySubMode = mode;
|
secondarySubMode = mode;
|
||||||
@@ -571,20 +605,15 @@ const startupState = runStartupBootstrapRuntimeService({
|
|||||||
subtitleWsService.start(port, () => currentSubText);
|
subtitleWsService.start(port, () => currentSubText);
|
||||||
},
|
},
|
||||||
log: (message) => appLogger.logInfo(message),
|
log: (message) => appLogger.logInfo(message),
|
||||||
createMecabTokenizerAndCheck: async () =>
|
createMecabTokenizerAndCheck: async () => {
|
||||||
createMecabTokenizerAndCheckRuntimeService({
|
const tokenizer = new MecabTokenizer();
|
||||||
createMecabTokenizer: () => new MecabTokenizer(),
|
|
||||||
setMecabTokenizer: (tokenizer) => {
|
|
||||||
mecabTokenizer = tokenizer;
|
mecabTokenizer = tokenizer;
|
||||||
|
await tokenizer.checkAvailability();
|
||||||
},
|
},
|
||||||
}),
|
createSubtitleTimingTracker: () => {
|
||||||
createSubtitleTimingTracker: () =>
|
const tracker = new SubtitleTimingTracker();
|
||||||
createSubtitleTimingTrackerRuntimeService({
|
|
||||||
createSubtitleTimingTracker: () => new SubtitleTimingTracker(),
|
|
||||||
setSubtitleTimingTracker: (tracker) => {
|
|
||||||
subtitleTimingTracker = tracker;
|
subtitleTimingTracker = tracker;
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
loadYomitanExtension: async () => {
|
loadYomitanExtension: async () => {
|
||||||
await loadYomitanExtension();
|
await loadYomitanExtension();
|
||||||
},
|
},
|
||||||
@@ -596,53 +625,31 @@ const startupState = runStartupBootstrapRuntimeService({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onWillQuitCleanup: () => {
|
onWillQuitCleanup: () => {
|
||||||
runAppShutdownRuntimeService({
|
|
||||||
unregisterAllGlobalShortcuts: () => {
|
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
},
|
|
||||||
stopSubtitleWebsocket: () => {
|
|
||||||
subtitleWsService.stop();
|
subtitleWsService.stop();
|
||||||
},
|
|
||||||
stopTexthookerService: () => {
|
|
||||||
texthookerService.stop();
|
texthookerService.stop();
|
||||||
},
|
|
||||||
destroyYomitanParserWindow: () => {
|
|
||||||
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
||||||
yomitanParserWindow.destroy();
|
yomitanParserWindow.destroy();
|
||||||
}
|
}
|
||||||
yomitanParserWindow = null;
|
yomitanParserWindow = null;
|
||||||
},
|
|
||||||
clearYomitanParserPromises: () => {
|
|
||||||
yomitanParserReadyPromise = null;
|
yomitanParserReadyPromise = null;
|
||||||
yomitanParserInitPromise = null;
|
yomitanParserInitPromise = null;
|
||||||
},
|
|
||||||
stopWindowTracker: () => {
|
|
||||||
if (windowTracker) {
|
if (windowTracker) {
|
||||||
windowTracker.stop();
|
windowTracker.stop();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
destroyMpvSocket: () => {
|
|
||||||
if (mpvClient && mpvClient.socket) {
|
if (mpvClient && mpvClient.socket) {
|
||||||
mpvClient.socket.destroy();
|
mpvClient.socket.destroy();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
clearReconnectTimer: () => {
|
|
||||||
if (reconnectTimer) {
|
if (reconnectTimer) {
|
||||||
clearTimeout(reconnectTimer);
|
clearTimeout(reconnectTimer);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
destroySubtitleTimingTracker: () => {
|
|
||||||
if (subtitleTimingTracker) {
|
if (subtitleTimingTracker) {
|
||||||
subtitleTimingTracker.destroy();
|
subtitleTimingTracker.destroy();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
destroyAnkiIntegration: () => {
|
|
||||||
if (ankiIntegration) {
|
if (ankiIntegration) {
|
||||||
ankiIntegration.destroy();
|
ankiIntegration.destroy();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
|
||||||
},
|
|
||||||
shouldRestoreWindowsOnActivate: () =>
|
shouldRestoreWindowsOnActivate: () =>
|
||||||
overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,
|
overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,
|
||||||
restoreWindowsOnActivate: () => {
|
restoreWindowsOnActivate: () => {
|
||||||
@@ -683,7 +690,9 @@ function handleCliCommand(
|
|||||||
},
|
},
|
||||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||||
openInBrowser: (url) => {
|
openInBrowser: (url) => {
|
||||||
shell.openExternal(url);
|
void shell.openExternal(url).catch((error) => {
|
||||||
|
console.error(`Failed to open browser for texthooker URL: ${url}`, error);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
@@ -898,15 +907,6 @@ function initializeOverlayRuntime(): void {
|
|||||||
overlayRuntimeInitialized = true;
|
overlayRuntimeInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShortcutUiRuntimeDeps() {
|
|
||||||
return {
|
|
||||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
|
||||||
getOverlayShortcutFallbackHandlers: () =>
|
|
||||||
getOverlayShortcutRuntimeHandlers().fallbackHandlers,
|
|
||||||
shortcutMatcher: shortcutMatchesInputForLocalFallback,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function openYomitanSettings(): void {
|
function openYomitanSettings(): void {
|
||||||
openYomitanSettingsWindow(
|
openYomitanSettingsWindow(
|
||||||
{
|
{
|
||||||
@@ -963,9 +963,11 @@ function getOverlayShortcutRuntimeHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
||||||
return runOverlayShortcutLocalFallbackRuntimeService(
|
return runOverlayShortcutLocalFallback(
|
||||||
input,
|
input,
|
||||||
getShortcutUiRuntimeDeps(),
|
getConfiguredShortcuts(),
|
||||||
|
shortcutMatchesInputForLocalFallback,
|
||||||
|
getOverlayShortcutRuntimeHandlers().fallbackHandlers,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1275,11 +1277,11 @@ function toggleInvisibleOverlay(): void {
|
|||||||
function setOverlayVisible(visible: boolean): void { setVisibleOverlayVisible(visible); }
|
function setOverlayVisible(visible: boolean): void { setVisibleOverlayVisible(visible); }
|
||||||
function toggleOverlay(): void { toggleVisibleOverlay(); }
|
function toggleOverlay(): void { toggleVisibleOverlay(); }
|
||||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||||
handleOverlayModalClosedService(
|
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||||
restoreVisibleOverlayOnModalClose,
|
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||||
modal,
|
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||||
(visible) => setVisibleOverlayVisible(visible),
|
setVisibleOverlayVisible(false);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||||
@@ -1381,10 +1383,8 @@ registerAnkiJimakuIpcRuntimeService(
|
|||||||
showDesktopNotification,
|
showDesktopNotification,
|
||||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||||
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
getFieldGroupingResolver: () => fieldGroupingResolver,
|
getFieldGroupingResolver: () => getFieldGroupingResolver(),
|
||||||
setFieldGroupingResolver: (resolver) => {
|
setFieldGroupingResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||||
fieldGroupingResolver = resolver;
|
|
||||||
},
|
|
||||||
parseMediaInfo: (mediaPath) => parseMediaInfo(mediaPath),
|
parseMediaInfo: (mediaPath) => parseMediaInfo(mediaPath),
|
||||||
getCurrentMediaPath: () => currentMediaPath,
|
getCurrentMediaPath: () => currentMediaPath,
|
||||||
jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
|
jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
|
||||||
|
|||||||
Reference in New Issue
Block a user