mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor: extract ipc mpv and tokenizer runtime deps
This commit is contained in:
@@ -6,26 +6,31 @@ import {
|
||||
SubsyncResult,
|
||||
} from "../../types";
|
||||
|
||||
export interface HandleMpvCommandFromIpcOptions {
|
||||
specialCommands: {
|
||||
SUBSYNC_TRIGGER: string;
|
||||
RUNTIME_OPTIONS_OPEN: string;
|
||||
RUNTIME_OPTION_CYCLE_PREFIX: string;
|
||||
REPLAY_SUBTITLE: string;
|
||||
PLAY_NEXT_SUBTITLE: string;
|
||||
};
|
||||
triggerSubsyncFromConfig: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
runtimeOptionsCycle: (
|
||||
id: RuntimeOptionId,
|
||||
direction: 1 | -1,
|
||||
) => RuntimeOptionApplyResult;
|
||||
showMpvOsd: (text: string) => void;
|
||||
mpvReplaySubtitle: () => void;
|
||||
mpvPlayNextSubtitle: () => void;
|
||||
mpvSendCommand: (command: (string | number)[]) => void;
|
||||
isMpvConnected: () => boolean;
|
||||
hasRuntimeOptionsManager: () => boolean;
|
||||
}
|
||||
|
||||
export function handleMpvCommandFromIpcService(
|
||||
command: (string | number)[],
|
||||
options: {
|
||||
specialCommands: {
|
||||
SUBSYNC_TRIGGER: string;
|
||||
RUNTIME_OPTIONS_OPEN: string;
|
||||
RUNTIME_OPTION_CYCLE_PREFIX: string;
|
||||
REPLAY_SUBTITLE: string;
|
||||
PLAY_NEXT_SUBTITLE: string;
|
||||
};
|
||||
triggerSubsyncFromConfig: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
runtimeOptionsCycle: (id: RuntimeOptionId, direction: 1 | -1) => RuntimeOptionApplyResult;
|
||||
showMpvOsd: (text: string) => void;
|
||||
mpvReplaySubtitle: () => void;
|
||||
mpvPlayNextSubtitle: () => void;
|
||||
mpvSendCommand: (command: (string | number)[]) => void;
|
||||
isMpvConnected: () => boolean;
|
||||
hasRuntimeOptionsManager: () => boolean;
|
||||
},
|
||||
options: HandleMpvCommandFromIpcOptions,
|
||||
): void {
|
||||
const first = typeof command[0] === "string" ? command[0] : "";
|
||||
if (first === options.specialCommands.SUBSYNC_TRIGGER) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { SPECIAL_COMMANDS } from "../../config";
|
||||
import { createMpvCommandIpcDepsRuntimeService } from "./mpv-command-ipc-deps-runtime-service";
|
||||
|
||||
test("createMpvCommandIpcDepsRuntimeService wires runtime-options cycle and manager availability", () => {
|
||||
const osd: string[] = [];
|
||||
const deps = createMpvCommandIpcDepsRuntimeService({
|
||||
specialCommands: SPECIAL_COMMANDS,
|
||||
triggerSubsyncFromConfig: () => {},
|
||||
openRuntimeOptionsPalette: () => {},
|
||||
getRuntimeOptionsManager: () => ({
|
||||
cycleOption: () => ({ ok: true, osdMessage: "cycled" }),
|
||||
}),
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
mpvReplaySubtitle: () => {},
|
||||
mpvPlayNextSubtitle: () => {},
|
||||
mpvSendCommand: () => {},
|
||||
isMpvConnected: () => true,
|
||||
});
|
||||
|
||||
const result = deps.runtimeOptionsCycle("subtitles.secondaryMode" as never, 1);
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(deps.hasRuntimeOptionsManager(), true);
|
||||
assert.ok(osd.includes("cycled"));
|
||||
});
|
||||
53
src/core/services/mpv-command-ipc-deps-runtime-service.ts
Normal file
53
src/core/services/mpv-command-ipc-deps-runtime-service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
RuntimeOptionApplyResult,
|
||||
RuntimeOptionId,
|
||||
} from "../../types";
|
||||
import {
|
||||
HandleMpvCommandFromIpcOptions,
|
||||
} from "./ipc-command-service";
|
||||
import { applyRuntimeOptionResultRuntimeService } from "./runtime-options-runtime-service";
|
||||
|
||||
interface RuntimeOptionsManagerLike {
|
||||
cycleOption: (
|
||||
id: RuntimeOptionId,
|
||||
direction: 1 | -1,
|
||||
) => RuntimeOptionApplyResult;
|
||||
}
|
||||
|
||||
export interface MpvCommandIpcDepsRuntimeOptions {
|
||||
specialCommands: HandleMpvCommandFromIpcOptions["specialCommands"];
|
||||
triggerSubsyncFromConfig: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
|
||||
showMpvOsd: (text: string) => void;
|
||||
mpvReplaySubtitle: () => void;
|
||||
mpvPlayNextSubtitle: () => void;
|
||||
mpvSendCommand: (command: (string | number)[]) => void;
|
||||
isMpvConnected: () => boolean;
|
||||
}
|
||||
|
||||
export function createMpvCommandIpcDepsRuntimeService(
|
||||
options: MpvCommandIpcDepsRuntimeOptions,
|
||||
): HandleMpvCommandFromIpcOptions {
|
||||
return {
|
||||
specialCommands: options.specialCommands,
|
||||
triggerSubsyncFromConfig: options.triggerSubsyncFromConfig,
|
||||
openRuntimeOptionsPalette: options.openRuntimeOptionsPalette,
|
||||
runtimeOptionsCycle: (id, direction) => {
|
||||
const manager = options.getRuntimeOptionsManager();
|
||||
if (!manager) {
|
||||
return { ok: false, error: "Runtime options manager unavailable" };
|
||||
}
|
||||
return applyRuntimeOptionResultRuntimeService(
|
||||
manager.cycleOption(id, direction),
|
||||
options.showMpvOsd,
|
||||
);
|
||||
},
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
mpvReplaySubtitle: options.mpvReplaySubtitle,
|
||||
mpvPlayNextSubtitle: options.mpvPlayNextSubtitle,
|
||||
mpvSendCommand: options.mpvSendCommand,
|
||||
isMpvConnected: options.isMpvConnected,
|
||||
hasRuntimeOptionsManager: () => options.getRuntimeOptionsManager() !== null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createRuntimeOptionsIpcDepsRuntimeService } from "./runtime-options-ipc-deps-runtime-service";
|
||||
|
||||
test("createRuntimeOptionsIpcDepsRuntimeService delegates set/cycle with osd", () => {
|
||||
const osd: string[] = [];
|
||||
const deps = createRuntimeOptionsIpcDepsRuntimeService({
|
||||
getRuntimeOptionsManager: () => ({
|
||||
setOptionValue: () => ({ ok: true, osdMessage: "set ok" }),
|
||||
cycleOption: () => ({ ok: true, osdMessage: "cycle ok" }),
|
||||
}),
|
||||
showMpvOsd: (text) => {
|
||||
osd.push(text);
|
||||
},
|
||||
});
|
||||
|
||||
const setResult = deps.setRuntimeOption("subtitles.secondaryMode", "hidden") as {
|
||||
ok: boolean;
|
||||
};
|
||||
const cycleResult = deps.cycleRuntimeOption("subtitles.secondaryMode", 1) as {
|
||||
ok: boolean;
|
||||
};
|
||||
|
||||
assert.equal(setResult.ok, true);
|
||||
assert.equal(cycleResult.ok, true);
|
||||
assert.ok(osd.includes("set ok"));
|
||||
assert.ok(osd.includes("cycle ok"));
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionValue,
|
||||
} from "../../types";
|
||||
import {
|
||||
cycleRuntimeOptionFromIpcRuntimeService,
|
||||
RuntimeOptionsManagerLike,
|
||||
setRuntimeOptionFromIpcRuntimeService,
|
||||
} from "./runtime-options-runtime-service";
|
||||
|
||||
export interface RuntimeOptionsIpcDepsRuntimeOptions {
|
||||
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}
|
||||
|
||||
export function createRuntimeOptionsIpcDepsRuntimeService(
|
||||
options: RuntimeOptionsIpcDepsRuntimeOptions,
|
||||
): {
|
||||
setRuntimeOption: (id: string, value: unknown) => unknown;
|
||||
cycleRuntimeOption: (id: string, direction: 1 | -1) => unknown;
|
||||
} {
|
||||
return {
|
||||
setRuntimeOption: (id, value) =>
|
||||
setRuntimeOptionFromIpcRuntimeService(
|
||||
options.getRuntimeOptionsManager(),
|
||||
id as RuntimeOptionId,
|
||||
value as RuntimeOptionValue,
|
||||
options.showMpvOsd,
|
||||
),
|
||||
cycleRuntimeOption: (id, direction) =>
|
||||
cycleRuntimeOptionFromIpcRuntimeService(
|
||||
options.getRuntimeOptionsManager(),
|
||||
id as RuntimeOptionId,
|
||||
direction,
|
||||
options.showMpvOsd,
|
||||
),
|
||||
};
|
||||
}
|
||||
48
src/core/services/tokenizer-deps-runtime-service.test.ts
Normal file
48
src/core/services/tokenizer-deps-runtime-service.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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, "猫");
|
||||
});
|
||||
45
src/core/services/tokenizer-deps-runtime-service.ts
Normal file
45
src/core/services/tokenizer-deps-runtime-service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
112
src/main.ts
112
src/main.ts
@@ -46,7 +46,6 @@ import * as os from "os";
|
||||
import * as fs from "fs";
|
||||
import * as crypto from "crypto";
|
||||
import { MecabTokenizer } from "./mecab-tokenizer";
|
||||
import { mergeTokens } from "./token-merger";
|
||||
import { BaseWindowTracker } from "./window-trackers";
|
||||
import {
|
||||
JimakuApiResponse,
|
||||
@@ -64,9 +63,7 @@ import {
|
||||
KikuFieldGroupingChoice,
|
||||
KikuMergePreviewRequest,
|
||||
KikuMergePreviewResponse,
|
||||
RuntimeOptionId,
|
||||
RuntimeOptionState,
|
||||
RuntimeOptionValue,
|
||||
MpvSubtitleRenderMetrics,
|
||||
} from "./types";
|
||||
import { SubtitleTimingTracker } from "./subtitle-timing-tracker";
|
||||
@@ -136,11 +133,6 @@ import {
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./core/services/mpv-runtime-service";
|
||||
import {
|
||||
applyRuntimeOptionResultRuntimeService,
|
||||
cycleRuntimeOptionFromIpcRuntimeService,
|
||||
setRuntimeOptionFromIpcRuntimeService,
|
||||
} from "./core/services/runtime-options-runtime-service";
|
||||
import {
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
@@ -206,6 +198,9 @@ import { createFieldGroupingOverlayRuntimeService } from "./core/services/field-
|
||||
import { createSubsyncRuntimeDepsService } from "./core/services/subsync-deps-runtime-service";
|
||||
import { createNumericShortcutRuntimeService } from "./core/services/numeric-shortcut-runtime-service";
|
||||
import { createOverlayVisibilityFacadeDepsRuntimeService } from "./core/services/overlay-visibility-facade-deps-runtime-service";
|
||||
import { createMpvCommandIpcDepsRuntimeService } from "./core/services/mpv-command-ipc-deps-runtime-service";
|
||||
import { createRuntimeOptionsIpcDepsRuntimeService } from "./core/services/runtime-options-ipc-deps-runtime-service";
|
||||
import { createTokenizerDepsRuntimeService } from "./core/services/tokenizer-deps-runtime-service";
|
||||
import { createRuntimeOptionsManagerRuntimeService } from "./core/services/runtime-options-manager-runtime-service";
|
||||
import { createAppLoggingRuntimeService } from "./core/services/app-logging-runtime-service";
|
||||
import {
|
||||
@@ -753,31 +748,25 @@ function updateMpvSubtitleRenderMetrics(
|
||||
}
|
||||
|
||||
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
||||
return tokenizeSubtitleService(text, {
|
||||
getYomitanExt: () => yomitanExt,
|
||||
getYomitanParserWindow: () => yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
yomitanParserWindow = window;
|
||||
},
|
||||
getYomitanParserReadyPromise: () => yomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
yomitanParserReadyPromise = promise;
|
||||
},
|
||||
getYomitanParserInitPromise: () => yomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
yomitanParserInitPromise = promise;
|
||||
},
|
||||
tokenizeWithMecab: async (tokenizeText) => {
|
||||
if (!mecabTokenizer) {
|
||||
return null;
|
||||
}
|
||||
const rawTokens = await mecabTokenizer.tokenize(tokenizeText);
|
||||
if (!rawTokens || rawTokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return mergeTokens(rawTokens);
|
||||
},
|
||||
});
|
||||
return tokenizeSubtitleService(
|
||||
text,
|
||||
createTokenizerDepsRuntimeService({
|
||||
getYomitanExt: () => yomitanExt,
|
||||
getYomitanParserWindow: () => yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
yomitanParserWindow = window;
|
||||
},
|
||||
getYomitanParserReadyPromise: () => yomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
yomitanParserReadyPromise = promise;
|
||||
},
|
||||
getYomitanParserInitPromise: () => yomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
yomitanParserInitPromise = promise;
|
||||
},
|
||||
getMecabTokenizer: () => mecabTokenizer,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function updateOverlayBounds(geometry: WindowGeometry): void {
|
||||
@@ -1218,27 +1207,21 @@ function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||
}
|
||||
|
||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||
handleMpvCommandFromIpcService(command, {
|
||||
specialCommands: SPECIAL_COMMANDS,
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
runtimeOptionsCycle: (id, direction) => {
|
||||
if (!runtimeOptionsManager) {
|
||||
return { ok: false, error: "Runtime options manager unavailable" };
|
||||
}
|
||||
return applyRuntimeOptionResultRuntimeService(
|
||||
runtimeOptionsManager.cycleOption(id, direction),
|
||||
(text) => showMpvOsd(text),
|
||||
);
|
||||
},
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(mpvClient),
|
||||
mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(mpvClient),
|
||||
mpvSendCommand: (rawCommand) =>
|
||||
sendMpvCommandRuntimeService(mpvClient, rawCommand),
|
||||
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||
hasRuntimeOptionsManager: () => runtimeOptionsManager !== null,
|
||||
});
|
||||
handleMpvCommandFromIpcService(
|
||||
command,
|
||||
createMpvCommandIpcDepsRuntimeService({
|
||||
specialCommands: SPECIAL_COMMANDS,
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
getRuntimeOptionsManager: () => runtimeOptionsManager,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(mpvClient),
|
||||
mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(mpvClient),
|
||||
mpvSendCommand: (rawCommand) =>
|
||||
sendMpvCommandRuntimeService(mpvClient, rawCommand),
|
||||
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function runSubsyncManualFromIpc(
|
||||
@@ -1247,6 +1230,11 @@ async function runSubsyncManualFromIpc(
|
||||
return runSubsyncManualFromIpcRuntimeService(request, getSubsyncRuntimeDeps());
|
||||
}
|
||||
|
||||
const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDepsRuntimeService({
|
||||
getRuntimeOptionsManager: () => runtimeOptionsManager,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
});
|
||||
|
||||
registerIpcHandlersService(
|
||||
createIpcDepsRuntimeService({
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
@@ -1274,20 +1262,8 @@ registerIpcHandlersService(
|
||||
runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
|
||||
getAnkiConnectStatus: () => ankiIntegration !== null,
|
||||
getRuntimeOptions: () => getRuntimeOptionsState(),
|
||||
setRuntimeOption: (id, value) =>
|
||||
setRuntimeOptionFromIpcRuntimeService(
|
||||
runtimeOptionsManager,
|
||||
id as RuntimeOptionId,
|
||||
value as RuntimeOptionValue,
|
||||
(text) => showMpvOsd(text),
|
||||
),
|
||||
cycleRuntimeOption: (id, direction) =>
|
||||
cycleRuntimeOptionFromIpcRuntimeService(
|
||||
runtimeOptionsManager,
|
||||
id as RuntimeOptionId,
|
||||
direction,
|
||||
(text) => showMpvOsd(text),
|
||||
),
|
||||
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
|
||||
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user