mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract ipc deps runtime service
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
||||||
"test:config": "pnpm run build && node --test dist/config/config.test.js",
|
"test:config": "pnpm run build && node --test dist/config/config.test.js",
|
||||||
"test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js",
|
"test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js",
|
||||||
"test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js",
|
"test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js",
|
||||||
"generate:config-example": "pnpm run build && node dist/generate-config-example.js",
|
"generate:config-example": "pnpm run build && node dist/generate-config-example.js",
|
||||||
"start": "pnpm run build && electron . --start",
|
"start": "pnpm run build && electron . --start",
|
||||||
|
|||||||
108
src/core/services/ipc-deps-runtime-service.test.ts
Normal file
108
src/core/services/ipc-deps-runtime-service.test.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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(), "");
|
||||||
|
});
|
||||||
100
src/core/services/ipc-deps-runtime-service.ts
Normal file
100
src/core/services/ipc-deps-runtime-service.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
49
src/main.ts
49
src/main.ts
@@ -206,6 +206,7 @@ import { runAppShutdownRuntimeService } from "./core/services/app-shutdown-runti
|
|||||||
import { createMpvIpcClientDepsRuntimeService } from "./core/services/mpv-client-deps-runtime-service";
|
import { createMpvIpcClientDepsRuntimeService } from "./core/services/mpv-client-deps-runtime-service";
|
||||||
import { createAppLifecycleDepsRuntimeService } from "./core/services/app-lifecycle-deps-runtime-service";
|
import { createAppLifecycleDepsRuntimeService } from "./core/services/app-lifecycle-deps-runtime-service";
|
||||||
import { createCliCommandDepsRuntimeService } from "./core/services/cli-command-deps-runtime-service";
|
import { createCliCommandDepsRuntimeService } from "./core/services/cli-command-deps-runtime-service";
|
||||||
|
import { createIpcDepsRuntimeService } from "./core/services/ipc-deps-runtime-service";
|
||||||
import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service";
|
import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service";
|
||||||
import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service";
|
import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service";
|
||||||
import {
|
import {
|
||||||
@@ -1243,63 +1244,49 @@ async function runSubsyncManualFromIpc(
|
|||||||
return runSubsyncManualFromIpcRuntimeService(request, getSubsyncRuntimeDeps());
|
return runSubsyncManualFromIpcRuntimeService(request, getSubsyncRuntimeDeps());
|
||||||
}
|
}
|
||||||
|
|
||||||
registerIpcHandlersService({
|
registerIpcHandlersService(
|
||||||
|
createIpcDepsRuntimeService({
|
||||||
getInvisibleWindow: () => invisibleWindow,
|
getInvisibleWindow: () => invisibleWindow,
|
||||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
getMainWindow: () => mainWindow,
|
||||||
setInvisibleIgnoreMouseEvents: (ignore, options) => {
|
getVisibleOverlayVisibility: () => visibleOverlayVisible,
|
||||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
getInvisibleOverlayVisibility: () => invisibleOverlayVisible,
|
||||||
invisibleWindow.setIgnoreMouseEvents(ignore, options);
|
|
||||||
},
|
|
||||||
onOverlayModalClosed: (modal) =>
|
onOverlayModalClosed: (modal) =>
|
||||||
handleOverlayModalClosed(modal as OverlayHostedModal),
|
handleOverlayModalClosed(modal as OverlayHostedModal),
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
quitApp: () => app.quit(),
|
quitApp: () => app.quit(),
|
||||||
toggleDevTools: () => {
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.toggleDevTools();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getVisibleOverlayVisibility: () => visibleOverlayVisible,
|
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
getInvisibleOverlayVisibility: () => invisibleOverlayVisible,
|
|
||||||
tokenizeCurrentSubtitle: () => tokenizeSubtitle(currentSubText),
|
tokenizeCurrentSubtitle: () => tokenizeSubtitle(currentSubText),
|
||||||
getCurrentSubtitleAss: () => currentSubAssText,
|
getCurrentSubtitleAss: () => currentSubAssText,
|
||||||
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics,
|
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics,
|
||||||
getSubtitlePosition: () => loadSubtitlePosition(),
|
getSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null,
|
getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null,
|
||||||
saveSubtitlePosition: (position) => saveSubtitlePosition(position as SubtitlePosition),
|
saveSubtitlePosition: (position) =>
|
||||||
getMecabStatus: () =>
|
saveSubtitlePosition(position as SubtitlePosition),
|
||||||
mecabTokenizer
|
getMecabTokenizer: () => mecabTokenizer,
|
||||||
? mecabTokenizer.getStatus()
|
|
||||||
: { available: false, enabled: false, path: null },
|
|
||||||
setMecabEnabled: (enabled) => {
|
|
||||||
if (mecabTokenizer) mecabTokenizer.setEnabled(enabled);
|
|
||||||
},
|
|
||||||
handleMpvCommand: (command) => handleMpvCommandFromIpc(command),
|
handleMpvCommand: (command) => handleMpvCommandFromIpc(command),
|
||||||
getKeybindings: () => keybindings,
|
getKeybindings: () => keybindings,
|
||||||
getSecondarySubMode: () => secondarySubMode,
|
getSecondarySubMode: () => secondarySubMode,
|
||||||
getCurrentSecondarySub: () => mpvClient?.currentSecondarySubText || "",
|
getMpvClient: () => mpvClient,
|
||||||
runSubsyncManual: (request) =>
|
runSubsyncManual: (request) =>
|
||||||
runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
|
runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
|
||||||
getAnkiConnectStatus: () => ankiIntegration !== null,
|
getAnkiConnectStatus: () => ankiIntegration !== null,
|
||||||
getRuntimeOptions: () => getRuntimeOptionsState(),
|
getRuntimeOptions: () => getRuntimeOptionsState(),
|
||||||
setRuntimeOption: (id, value) => {
|
setRuntimeOption: (id, value) =>
|
||||||
return setRuntimeOptionFromIpcRuntimeService(
|
setRuntimeOptionFromIpcRuntimeService(
|
||||||
runtimeOptionsManager,
|
runtimeOptionsManager,
|
||||||
id as RuntimeOptionId,
|
id as RuntimeOptionId,
|
||||||
value as RuntimeOptionValue,
|
value as RuntimeOptionValue,
|
||||||
(text) => showMpvOsd(text),
|
(text) => showMpvOsd(text),
|
||||||
);
|
),
|
||||||
},
|
cycleRuntimeOption: (id, direction) =>
|
||||||
cycleRuntimeOption: (id, direction) => {
|
cycleRuntimeOptionFromIpcRuntimeService(
|
||||||
return cycleRuntimeOptionFromIpcRuntimeService(
|
|
||||||
runtimeOptionsManager,
|
runtimeOptionsManager,
|
||||||
id as RuntimeOptionId,
|
id as RuntimeOptionId,
|
||||||
direction,
|
direction,
|
||||||
(text) => showMpvOsd(text),
|
(text) => showMpvOsd(text),
|
||||||
);
|
),
|
||||||
},
|
}),
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and show a desktop notification with robust icon handling.
|
* Create and show a desktop notification with robust icon handling.
|
||||||
|
|||||||
Reference in New Issue
Block a user