mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor(core): normalize core service naming
Standardize core service module and export names to reduce naming ambiguity and make imports predictable across runtime, tests, scripts, and docs.
This commit is contained in:
@@ -2,8 +2,8 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
AnkiJimakuIpcRuntimeOptions,
|
||||
registerAnkiJimakuIpcRuntimeService,
|
||||
} from "./anki-jimaku-service";
|
||||
registerAnkiJimakuIpcRuntime,
|
||||
} from "./anki-jimaku";
|
||||
|
||||
interface RuntimeHarness {
|
||||
options: AnkiJimakuIpcRuntimeOptions;
|
||||
@@ -92,7 +92,7 @@ function createHarness(): RuntimeHarness {
|
||||
};
|
||||
|
||||
let registered: Record<string, (...args: unknown[]) => unknown> = {};
|
||||
registerAnkiJimakuIpcRuntimeService(
|
||||
registerAnkiJimakuIpcRuntime(
|
||||
options,
|
||||
(deps) => {
|
||||
registered = deps as unknown as Record<string, (...args: unknown[]) => unknown>;
|
||||
@@ -102,7 +102,7 @@ function createHarness(): RuntimeHarness {
|
||||
return { options, registered, state };
|
||||
}
|
||||
|
||||
test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () => {
|
||||
test("registerAnkiJimakuIpcRuntime provides full handler surface", () => {
|
||||
const { registered } = createHarness();
|
||||
const expected = [
|
||||
"setAnkiConnectEnabled",
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
KikuFieldGroupingRequestData,
|
||||
} from "../../types";
|
||||
import { sortJimakuFiles } from "../../jimaku/utils";
|
||||
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc-service";
|
||||
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
export type RegisterAnkiJimakuIpcRuntimeHandler = (
|
||||
@@ -65,7 +65,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
|
||||
const logger = createLogger("main:anki-jimaku");
|
||||
|
||||
export function registerAnkiJimakuIpcRuntimeService(
|
||||
export function registerAnkiJimakuIpcRuntime(
|
||||
options: AnkiJimakuIpcRuntimeOptions,
|
||||
registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
|
||||
): void {
|
||||
@@ -44,7 +44,7 @@ export interface AppLifecycleDepsRuntimeOptions {
|
||||
restoreWindowsOnActivate: () => void;
|
||||
}
|
||||
|
||||
export function createAppLifecycleDepsRuntimeService(
|
||||
export function createAppLifecycleDepsRuntime(
|
||||
options: AppLifecycleDepsRuntimeOptions,
|
||||
): AppLifecycleServiceDeps {
|
||||
return {
|
||||
@@ -80,7 +80,7 @@ export function createAppLifecycleDepsRuntimeService(
|
||||
};
|
||||
}
|
||||
|
||||
export function startAppLifecycleService(
|
||||
export function startAppLifecycle(
|
||||
initialArgs: CliArgs,
|
||||
deps: AppLifecycleServiceDeps,
|
||||
): void {
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { AppReadyRuntimeDeps, runAppReadyRuntimeService } from "./startup-service";
|
||||
import { AppReadyRuntimeDeps, runAppReadyRuntime } from "./startup";
|
||||
|
||||
function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||
const calls: string[] = [];
|
||||
@@ -37,11 +37,11 @@ function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
|
||||
return { deps, calls };
|
||||
}
|
||||
|
||||
test("runAppReadyRuntimeService starts websocket in auto mode when plugin missing", async () => {
|
||||
test("runAppReadyRuntime starts websocket in auto mode when plugin missing", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
hasMpvWebsocketPlugin: () => false,
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
await runAppReadyRuntime(deps);
|
||||
assert.ok(calls.includes("startSubtitleWebsocket:9001"));
|
||||
assert.ok(calls.includes("initializeOverlayRuntime"));
|
||||
assert.ok(calls.includes("createImmersionTracker"));
|
||||
@@ -80,11 +80,11 @@ test("runAppReadyRuntimeService logs and continues when createImmersionTracker t
|
||||
assert.ok(calls.includes("handleInitialArgs"));
|
||||
});
|
||||
|
||||
test("runAppReadyRuntimeService logs defer message when overlay not auto-started", async () => {
|
||||
test("runAppReadyRuntime logs defer message when overlay not auto-started", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
await runAppReadyRuntime(deps);
|
||||
assert.ok(
|
||||
calls.includes(
|
||||
"log:Overlay runtime deferred: waiting for explicit overlay command.",
|
||||
@@ -92,7 +92,7 @@ test("runAppReadyRuntimeService logs defer message when overlay not auto-started
|
||||
);
|
||||
});
|
||||
|
||||
test("runAppReadyRuntimeService applies config logging level during app-ready", async () => {
|
||||
test("runAppReadyRuntime applies config logging level during app-ready", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
getResolvedConfig: () => ({
|
||||
websocket: { enabled: "auto" },
|
||||
@@ -100,6 +100,6 @@ test("runAppReadyRuntimeService applies config logging level during app-ready",
|
||||
logging: { level: "warn" },
|
||||
}),
|
||||
});
|
||||
await runAppReadyRuntimeService(deps);
|
||||
await runAppReadyRuntime(deps);
|
||||
assert.ok(calls.includes("setLogLevel:warn:config"));
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { CliArgs } from "../../cli/args";
|
||||
import { CliCommandServiceDeps, handleCliCommandService } from "./cli-command-service";
|
||||
import { CliCommandServiceDeps, handleCliCommand } from "./cli-command";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
@@ -148,21 +148,21 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
return { deps, calls, osd };
|
||||
}
|
||||
|
||||
test("handleCliCommandService ignores --start for second-instance without actions", () => {
|
||||
test("handleCliCommand ignores --start for second-instance without actions", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
const args = makeArgs({ start: true });
|
||||
|
||||
handleCliCommandService(args, "second-instance", deps);
|
||||
handleCliCommand(args, "second-instance", deps);
|
||||
|
||||
assert.ok(calls.includes("log:Ignoring --start because SubMiner is already running."));
|
||||
assert.equal(calls.some((value) => value.includes("connectMpvClient")), false);
|
||||
});
|
||||
|
||||
test("handleCliCommandService runs texthooker flow with browser open", () => {
|
||||
test("handleCliCommand runs texthooker flow with browser open", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
const args = makeArgs({ texthooker: true });
|
||||
|
||||
handleCliCommandService(args, "initial", deps);
|
||||
handleCliCommand(args, "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("ensureTexthookerRunning:5174"));
|
||||
assert.ok(
|
||||
@@ -170,24 +170,24 @@ test("handleCliCommandService runs texthooker flow with browser open", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("handleCliCommandService reports async mine errors to OSD", async () => {
|
||||
test("handleCliCommand reports async mine errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
mineSentenceCard: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommandService(makeArgs({ mineSentence: true }), "initial", deps);
|
||||
handleCliCommand(makeArgs({ mineSentence: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(calls.some((value) => value.startsWith("error:mineSentenceCard failed:")));
|
||||
assert.ok(osd.some((value) => value.includes("Mine sentence failed: boom")));
|
||||
});
|
||||
|
||||
test("handleCliCommandService applies socket path and connects on start", () => {
|
||||
test("handleCliCommand applies socket path and connects on start", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
handleCliCommandService(
|
||||
handleCliCommand(
|
||||
makeArgs({ start: true, socketPath: "/tmp/custom.sock" }),
|
||||
"initial",
|
||||
deps,
|
||||
@@ -198,12 +198,12 @@ test("handleCliCommandService applies socket path and connects on start", () =>
|
||||
assert.ok(calls.includes("connectMpvClient"));
|
||||
});
|
||||
|
||||
test("handleCliCommandService warns when texthooker port override used while running", () => {
|
||||
test("handleCliCommand warns when texthooker port override used while running", () => {
|
||||
const { deps, calls } = createDeps({
|
||||
isTexthookerRunning: () => true,
|
||||
});
|
||||
|
||||
handleCliCommandService(
|
||||
handleCliCommand(
|
||||
makeArgs({ texthookerPort: 9999, texthooker: true }),
|
||||
"initial",
|
||||
deps,
|
||||
@@ -217,25 +217,25 @@ test("handleCliCommandService warns when texthooker port override used while run
|
||||
assert.equal(calls.some((value) => value === "setTexthookerPort:9999"), false);
|
||||
});
|
||||
|
||||
test("handleCliCommandService prints help and stops app when no window exists", () => {
|
||||
test("handleCliCommand prints help and stops app when no window exists", () => {
|
||||
const { deps, calls } = createDeps({
|
||||
hasMainWindow: () => false,
|
||||
});
|
||||
|
||||
handleCliCommandService(makeArgs({ help: true }), "initial", deps);
|
||||
handleCliCommand(makeArgs({ help: true }), "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("printHelp"));
|
||||
assert.ok(calls.includes("stopApp"));
|
||||
});
|
||||
|
||||
test("handleCliCommandService reports async trigger-subsync errors to OSD", async () => {
|
||||
test("handleCliCommand reports async trigger-subsync errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
triggerSubsyncFromConfig: async () => {
|
||||
throw new Error("subsync boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommandService(makeArgs({ triggerSubsync: true }), "initial", deps);
|
||||
handleCliCommand(makeArgs({ triggerSubsync: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(
|
||||
@@ -244,16 +244,16 @@ test("handleCliCommandService reports async trigger-subsync errors to OSD", asyn
|
||||
assert.ok(osd.some((value) => value.includes("Subsync failed: subsync boom")));
|
||||
});
|
||||
|
||||
test("handleCliCommandService stops app for --stop command", () => {
|
||||
test("handleCliCommand stops app for --stop command", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommandService(makeArgs({ stop: true }), "initial", deps);
|
||||
handleCliCommand(makeArgs({ stop: true }), "initial", deps);
|
||||
assert.ok(calls.includes("log:Stopping SubMiner..."));
|
||||
assert.ok(calls.includes("stopApp"));
|
||||
});
|
||||
|
||||
test("handleCliCommandService still runs non-start actions on second-instance", () => {
|
||||
test("handleCliCommand still runs non-start actions on second-instance", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommandService(
|
||||
handleCliCommand(
|
||||
makeArgs({ start: true, toggleVisibleOverlay: true }),
|
||||
"second-instance",
|
||||
deps,
|
||||
@@ -262,7 +262,7 @@ test("handleCliCommandService still runs non-start actions on second-instance",
|
||||
assert.equal(calls.some((value) => value === "connectMpvClient"), true);
|
||||
});
|
||||
|
||||
test("handleCliCommandService handles visibility and utility command dispatches", () => {
|
||||
test("handleCliCommand handles visibility and utility command dispatches", () => {
|
||||
const cases: Array<{
|
||||
args: Partial<CliArgs>;
|
||||
expected: string;
|
||||
@@ -285,7 +285,7 @@ test("handleCliCommandService handles visibility and utility command dispatches"
|
||||
|
||||
for (const entry of cases) {
|
||||
const { deps, calls } = createDeps();
|
||||
handleCliCommandService(makeArgs(entry.args), "initial", deps);
|
||||
handleCliCommand(makeArgs(entry.args), "initial", deps);
|
||||
assert.ok(
|
||||
calls.includes(entry.expected),
|
||||
`expected call missing for args ${JSON.stringify(entry.args)}: ${entry.expected}`,
|
||||
@@ -293,22 +293,22 @@ test("handleCliCommandService handles visibility and utility command dispatches"
|
||||
}
|
||||
});
|
||||
|
||||
test("handleCliCommandService runs refresh-known-words command", () => {
|
||||
test("handleCliCommand runs refresh-known-words command", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
handleCliCommand(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("refreshKnownWords"));
|
||||
});
|
||||
|
||||
test("handleCliCommandService reports async refresh-known-words errors to OSD", async () => {
|
||||
test("handleCliCommand reports async refresh-known-words errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
refreshKnownWords: async () => {
|
||||
throw new Error("refresh boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
handleCliCommand(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(
|
||||
@@ -116,7 +116,7 @@ export interface CliCommandDepsRuntimeOptions {
|
||||
error: (message: string, err: unknown) => void;
|
||||
}
|
||||
|
||||
export function createCliCommandDepsRuntimeService(
|
||||
export function createCliCommandDepsRuntime(
|
||||
options: CliCommandDepsRuntimeOptions,
|
||||
): CliCommandServiceDeps {
|
||||
return {
|
||||
@@ -189,7 +189,7 @@ function runAsyncWithOsd(
|
||||
});
|
||||
}
|
||||
|
||||
export function handleCliCommandService(
|
||||
export function handleCliCommand(
|
||||
args: CliArgs,
|
||||
source: CliCommandSource = "initial",
|
||||
deps: CliCommandServiceDeps,
|
||||
@@ -1,15 +1,15 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { KikuFieldGroupingChoice } from "../../types";
|
||||
import { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
|
||||
import { createFieldGroupingOverlayRuntime } from "./field-grouping-overlay";
|
||||
|
||||
test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets restore flag", () => {
|
||||
test("createFieldGroupingOverlayRuntime sends overlay messages and sets restore flag", () => {
|
||||
const sent: unknown[][] = [];
|
||||
let visible = false;
|
||||
const restore = new Set<"runtime-options" | "subsync">();
|
||||
|
||||
const runtime =
|
||||
createFieldGroupingOverlayRuntimeService<"runtime-options" | "subsync">({
|
||||
createFieldGroupingOverlayRuntime<"runtime-options" | "subsync">({
|
||||
getMainWindow: () => ({
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
@@ -40,10 +40,10 @@ test("createFieldGroupingOverlayRuntimeService sends overlay messages and sets r
|
||||
assert.deepEqual(sent, [["runtime-options:open"]]);
|
||||
});
|
||||
|
||||
test("createFieldGroupingOverlayRuntimeService callback cancels when send fails", async () => {
|
||||
test("createFieldGroupingOverlayRuntime callback cancels when send fails", async () => {
|
||||
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
|
||||
const runtime =
|
||||
createFieldGroupingOverlayRuntimeService<"runtime-options" | "subsync">({
|
||||
createFieldGroupingOverlayRuntime<"runtime-options" | "subsync">({
|
||||
getMainWindow: () => null,
|
||||
getVisibleOverlayVisible: () => false,
|
||||
getInvisibleOverlayVisible: () => false,
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
KikuFieldGroupingRequestData,
|
||||
} from "../../types";
|
||||
import {
|
||||
createFieldGroupingCallbackRuntimeService,
|
||||
sendToVisibleOverlayRuntimeService,
|
||||
} from "./overlay-bridge-service";
|
||||
createFieldGroupingCallbackRuntime,
|
||||
sendToVisibleOverlayRuntime,
|
||||
} from "./overlay-bridge";
|
||||
|
||||
interface WindowLike {
|
||||
isDestroyed: () => boolean;
|
||||
@@ -32,7 +32,7 @@ export interface FieldGroupingOverlayRuntimeOptions<T extends string> {
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export function createFieldGroupingOverlayRuntimeService<T extends string>(
|
||||
export function createFieldGroupingOverlayRuntime<T extends string>(
|
||||
options: FieldGroupingOverlayRuntimeOptions<T>,
|
||||
): {
|
||||
sendToVisibleOverlay: (
|
||||
@@ -52,7 +52,7 @@ export function createFieldGroupingOverlayRuntimeService<T extends string>(
|
||||
if (options.sendToVisibleOverlay) {
|
||||
return options.sendToVisibleOverlay(channel, payload, runtimeOptions);
|
||||
}
|
||||
return sendToVisibleOverlayRuntimeService({
|
||||
return sendToVisibleOverlayRuntime({
|
||||
mainWindow: options.getMainWindow() as never,
|
||||
visibleOverlayVisible: options.getVisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
|
||||
@@ -67,7 +67,7 @@ export function createFieldGroupingOverlayRuntimeService<T extends string>(
|
||||
const createFieldGroupingCallback = (): ((
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>) => {
|
||||
return createFieldGroupingCallbackRuntimeService({
|
||||
return createFieldGroupingCallbackRuntime({
|
||||
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
|
||||
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
KikuFieldGroupingRequestData,
|
||||
} from "../../types";
|
||||
|
||||
export function createFieldGroupingCallbackService(options: {
|
||||
export function createFieldGroupingCallback(options: {
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
@@ -4,15 +4,15 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { createFrequencyDictionaryLookupService } from "./frequency-dictionary-service";
|
||||
import { createFrequencyDictionaryLookup } from "./frequency-dictionary";
|
||||
|
||||
test("createFrequencyDictionaryLookupService logs parse errors and returns no-op for invalid dictionaries", async () => {
|
||||
test("createFrequencyDictionaryLookup logs parse errors and returns no-op for invalid dictionaries", async () => {
|
||||
const logs: string[] = [];
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "subminer-frequency-dict-"));
|
||||
const bankPath = path.join(tempDir, "term_meta_bank_1.json");
|
||||
fs.writeFileSync(bankPath, "{ invalid json");
|
||||
|
||||
const lookup = await createFrequencyDictionaryLookupService({
|
||||
const lookup = await createFrequencyDictionaryLookup({
|
||||
searchPaths: [tempDir],
|
||||
log: (message) => {
|
||||
logs.push(message);
|
||||
@@ -31,10 +31,10 @@ test("createFrequencyDictionaryLookupService logs parse errors and returns no-op
|
||||
);
|
||||
});
|
||||
|
||||
test("createFrequencyDictionaryLookupService continues with no-op lookup when search path is missing", async () => {
|
||||
test("createFrequencyDictionaryLookup continues with no-op lookup when search path is missing", async () => {
|
||||
const logs: string[] = [];
|
||||
const missingPath = path.join(os.tmpdir(), "subminer-frequency-dict-missing-dir");
|
||||
const lookup = await createFrequencyDictionaryLookupService({
|
||||
const lookup = await createFrequencyDictionaryLookup({
|
||||
searchPaths: [missingPath],
|
||||
log: (message) => {
|
||||
logs.push(message);
|
||||
@@ -145,7 +145,7 @@ function collectDictionaryFromPath(
|
||||
return terms;
|
||||
}
|
||||
|
||||
export async function createFrequencyDictionaryLookupService(
|
||||
export async function createFrequencyDictionaryLookup(
|
||||
options: FrequencyDictionaryLookupOptions,
|
||||
): Promise<(term: string) => number | null> {
|
||||
const attemptedPaths: string[] = [];
|
||||
@@ -1,39 +1,39 @@
|
||||
export { TexthookerService } from "./texthooker-service";
|
||||
export { hasMpvWebsocketPlugin, SubtitleWebSocketService } from "./subtitle-ws-service";
|
||||
export { registerGlobalShortcutsService } from "./shortcut-service";
|
||||
export { createIpcDepsRuntimeService, registerIpcHandlersService } from "./ipc-service";
|
||||
export { shortcutMatchesInputForLocalFallback } from "./shortcut-fallback-service";
|
||||
export { Texthooker } from "./texthooker";
|
||||
export { hasMpvWebsocketPlugin, SubtitleWebSocket } from "./subtitle-ws";
|
||||
export { registerGlobalShortcuts } from "./shortcut";
|
||||
export { createIpcDepsRuntime, registerIpcHandlers } from "./ipc";
|
||||
export { shortcutMatchesInputForLocalFallback } from "./shortcut-fallback";
|
||||
export {
|
||||
refreshOverlayShortcutsRuntimeService,
|
||||
registerOverlayShortcutsService,
|
||||
syncOverlayShortcutsRuntimeService,
|
||||
unregisterOverlayShortcutsRuntimeService,
|
||||
} from "./overlay-shortcut-service";
|
||||
refreshOverlayShortcutsRuntime,
|
||||
registerOverlayShortcuts,
|
||||
syncOverlayShortcutsRuntime,
|
||||
unregisterOverlayShortcutsRuntime,
|
||||
} from "./overlay-shortcut";
|
||||
export { createOverlayShortcutRuntimeHandlers } from "./overlay-shortcut-handler";
|
||||
export { createCliCommandDepsRuntimeService, handleCliCommandService } from "./cli-command-service";
|
||||
export { createCliCommandDepsRuntime, handleCliCommand } from "./cli-command";
|
||||
export {
|
||||
copyCurrentSubtitleService,
|
||||
handleMineSentenceDigitService,
|
||||
handleMultiCopyDigitService,
|
||||
markLastCardAsAudioCardService,
|
||||
mineSentenceCardService,
|
||||
triggerFieldGroupingService,
|
||||
updateLastCardFromClipboardService,
|
||||
} from "./mining-service";
|
||||
export { createAppLifecycleDepsRuntimeService, startAppLifecycleService } from "./app-lifecycle-service";
|
||||
copyCurrentSubtitle,
|
||||
handleMineSentenceDigit,
|
||||
handleMultiCopyDigit,
|
||||
markLastCardAsAudioCard,
|
||||
mineSentenceCard,
|
||||
triggerFieldGrouping,
|
||||
updateLastCardFromClipboard,
|
||||
} from "./mining";
|
||||
export { createAppLifecycleDepsRuntime, startAppLifecycle } from "./app-lifecycle";
|
||||
export {
|
||||
cycleSecondarySubModeService,
|
||||
} from "./subtitle-position-service";
|
||||
cycleSecondarySubMode,
|
||||
} from "./subtitle-position";
|
||||
export {
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||
} from "./startup-service";
|
||||
export { openYomitanSettingsWindow } from "./yomitan-settings-service";
|
||||
export { createTokenizerDepsRuntimeService, tokenizeSubtitleService } from "./tokenizer-service";
|
||||
export { createFrequencyDictionaryLookupService } from "./frequency-dictionary-service";
|
||||
export { createJlptVocabularyLookupService } from "./jlpt-vocab-service";
|
||||
getInitialInvisibleOverlayVisibility,
|
||||
isAutoUpdateEnabledRuntime,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
shouldBindVisibleOverlayToMpvSubVisibility,
|
||||
} from "./startup";
|
||||
export { openYomitanSettingsWindow } from "./yomitan-settings";
|
||||
export { createTokenizerDepsRuntime, tokenizeSubtitle } from "./tokenizer";
|
||||
export { createFrequencyDictionaryLookup } from "./frequency-dictionary";
|
||||
export { createJlptVocabularyLookup } from "./jlpt-vocab";
|
||||
export {
|
||||
getIgnoredPos1Entries,
|
||||
JlptIgnoredPos1Entry,
|
||||
@@ -44,59 +44,59 @@ export {
|
||||
shouldIgnoreJlptByTerm,
|
||||
shouldIgnoreJlptForMecabPos1,
|
||||
} from "./jlpt-token-filter";
|
||||
export { loadYomitanExtensionService } from "./yomitan-extension-loader-service";
|
||||
export { loadYomitanExtension } from "./yomitan-extension-loader";
|
||||
export {
|
||||
getJimakuLanguagePreferenceService,
|
||||
getJimakuMaxEntryResultsService,
|
||||
jimakuFetchJsonService,
|
||||
resolveJimakuApiKeyService,
|
||||
} from "./jimaku-service";
|
||||
getJimakuLanguagePreference,
|
||||
getJimakuMaxEntryResults,
|
||||
jimakuFetchJson,
|
||||
resolveJimakuApiKey,
|
||||
} from "./jimaku";
|
||||
export {
|
||||
loadSubtitlePositionService,
|
||||
saveSubtitlePositionService,
|
||||
updateCurrentMediaPathService,
|
||||
} from "./subtitle-position-service";
|
||||
loadSubtitlePosition,
|
||||
saveSubtitlePosition,
|
||||
updateCurrentMediaPath,
|
||||
} from "./subtitle-position";
|
||||
export {
|
||||
createOverlayWindowService,
|
||||
enforceOverlayLayerOrderService,
|
||||
ensureOverlayWindowLevelService,
|
||||
updateOverlayWindowBoundsService,
|
||||
} from "./overlay-window-service";
|
||||
export { initializeOverlayRuntimeService } from "./overlay-runtime-init-service";
|
||||
createOverlayWindow,
|
||||
enforceOverlayLayerOrder,
|
||||
ensureOverlayWindowLevel,
|
||||
updateOverlayWindowBounds,
|
||||
} from "./overlay-window";
|
||||
export { initializeOverlayRuntime } from "./overlay-runtime-init";
|
||||
export {
|
||||
setInvisibleOverlayVisibleService,
|
||||
setVisibleOverlayVisibleService,
|
||||
syncInvisibleOverlayMousePassthroughService,
|
||||
updateInvisibleOverlayVisibilityService,
|
||||
updateVisibleOverlayVisibilityService,
|
||||
} from "./overlay-visibility-service";
|
||||
setInvisibleOverlayVisible,
|
||||
setVisibleOverlayVisible,
|
||||
syncInvisibleOverlayMousePassthrough,
|
||||
updateInvisibleOverlayVisibility,
|
||||
updateVisibleOverlayVisibility,
|
||||
} from "./overlay-visibility";
|
||||
export {
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
MpvIpcClient,
|
||||
MpvRuntimeClientLike,
|
||||
MpvTrackProperty,
|
||||
playNextSubtitleRuntimeService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
playNextSubtitleRuntime,
|
||||
replayCurrentSubtitleRuntime,
|
||||
resolveCurrentAudioStreamIndex,
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-service";
|
||||
sendMpvCommandRuntime,
|
||||
setMpvSubVisibilityRuntime,
|
||||
showMpvOsdRuntime,
|
||||
} from "./mpv";
|
||||
export {
|
||||
applyMpvSubtitleRenderMetricsPatchService,
|
||||
applyMpvSubtitleRenderMetricsPatch,
|
||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
sanitizeMpvSubtitleRenderMetrics,
|
||||
} from "./mpv-render-metrics-service";
|
||||
export { createOverlayContentMeasurementStoreService } from "./overlay-content-measurement-service";
|
||||
export { handleMpvCommandFromIpcService } from "./ipc-command-service";
|
||||
} from "./mpv-render-metrics";
|
||||
export { createOverlayContentMeasurementStore } from "./overlay-content-measurement";
|
||||
export { handleMpvCommandFromIpc } from "./ipc-command";
|
||||
export { createFieldGroupingOverlayRuntime } from "./field-grouping-overlay";
|
||||
export { createNumericShortcutRuntime } from "./numeric-shortcut";
|
||||
export { runStartupBootstrapRuntime } from "./startup";
|
||||
export { runSubsyncManualFromIpcRuntime, triggerSubsyncFromConfigRuntime } from "./subsync-runner";
|
||||
export { registerAnkiJimakuIpcRuntime } from "./anki-jimaku";
|
||||
export { ImmersionTrackerService } from "./immersion-tracker-service";
|
||||
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-service";
|
||||
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-service";
|
||||
export { runStartupBootstrapRuntimeService } from "./startup-service";
|
||||
export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runner-service";
|
||||
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-service";
|
||||
export {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
createOverlayManagerService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./overlay-manager-service";
|
||||
broadcastRuntimeOptionsChangedRuntime,
|
||||
createOverlayManager,
|
||||
setOverlayDebugVisualizationEnabledRuntime,
|
||||
} from "./overlay-manager";
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface HandleMpvCommandFromIpcOptions {
|
||||
hasRuntimeOptionsManager: () => boolean;
|
||||
}
|
||||
|
||||
export function handleMpvCommandFromIpcService(
|
||||
export function handleMpvCommandFromIpc(
|
||||
command: (string | number)[],
|
||||
options: HandleMpvCommandFromIpcOptions,
|
||||
): void {
|
||||
@@ -66,7 +66,7 @@ export function handleMpvCommandFromIpcService(
|
||||
}
|
||||
}
|
||||
|
||||
export async function runSubsyncManualFromIpcService(
|
||||
export async function runSubsyncManualFromIpc(
|
||||
request: SubsyncManualRunRequest,
|
||||
options: {
|
||||
isSubsyncInProgress: () => boolean;
|
||||
@@ -84,7 +84,7 @@ export interface IpcDepsRuntimeOptions {
|
||||
reportOverlayContentBounds: (payload: unknown) => void;
|
||||
}
|
||||
|
||||
export function createIpcDepsRuntimeService(
|
||||
export function createIpcDepsRuntime(
|
||||
options: IpcDepsRuntimeOptions,
|
||||
): IpcServiceDeps {
|
||||
return {
|
||||
@@ -143,7 +143,7 @@ export function createIpcDepsRuntimeService(
|
||||
};
|
||||
}
|
||||
|
||||
export function registerIpcHandlersService(deps: IpcServiceDeps): void {
|
||||
export function registerIpcHandlers(deps: IpcServiceDeps): void {
|
||||
ipcMain.on(
|
||||
"set-ignore-mouse-events",
|
||||
(
|
||||
@@ -8,34 +8,34 @@ import {
|
||||
resolveJimakuApiKey as resolveJimakuApiKeyFromConfig,
|
||||
} from "../../jimaku/utils";
|
||||
|
||||
export function getJimakuConfigService(
|
||||
export function getJimakuConfig(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
): JimakuConfig {
|
||||
const config = getResolvedConfig();
|
||||
return config.jimaku ?? {};
|
||||
}
|
||||
|
||||
export function getJimakuBaseUrlService(
|
||||
export function getJimakuBaseUrl(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
defaultBaseUrl: string,
|
||||
): string {
|
||||
const config = getJimakuConfigService(getResolvedConfig);
|
||||
const config = getJimakuConfig(getResolvedConfig);
|
||||
return config.apiBaseUrl || defaultBaseUrl;
|
||||
}
|
||||
|
||||
export function getJimakuLanguagePreferenceService(
|
||||
export function getJimakuLanguagePreference(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
defaultPreference: JimakuLanguagePreference,
|
||||
): JimakuLanguagePreference {
|
||||
const config = getJimakuConfigService(getResolvedConfig);
|
||||
const config = getJimakuConfig(getResolvedConfig);
|
||||
return config.languagePreference || defaultPreference;
|
||||
}
|
||||
|
||||
export function getJimakuMaxEntryResultsService(
|
||||
export function getJimakuMaxEntryResults(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
defaultValue: number,
|
||||
): number {
|
||||
const config = getJimakuConfigService(getResolvedConfig);
|
||||
const config = getJimakuConfig(getResolvedConfig);
|
||||
const value = config.maxEntryResults;
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
return Math.floor(value);
|
||||
@@ -43,13 +43,13 @@ export function getJimakuMaxEntryResultsService(
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export async function resolveJimakuApiKeyService(
|
||||
export async function resolveJimakuApiKey(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
): Promise<string | null> {
|
||||
return resolveJimakuApiKeyFromConfig(getJimakuConfigService(getResolvedConfig));
|
||||
return resolveJimakuApiKeyFromConfig(getJimakuConfig(getResolvedConfig));
|
||||
}
|
||||
|
||||
export async function jimakuFetchJsonService<T>(
|
||||
export async function jimakuFetchJson<T>(
|
||||
endpoint: string,
|
||||
query: Record<string, string | number | boolean | null | undefined> = {},
|
||||
options: {
|
||||
@@ -59,7 +59,7 @@ export async function jimakuFetchJsonService<T>(
|
||||
defaultLanguagePreference: JimakuLanguagePreference;
|
||||
},
|
||||
): Promise<JimakuApiResponse<T>> {
|
||||
const apiKey = await resolveJimakuApiKeyService(options.getResolvedConfig);
|
||||
const apiKey = await resolveJimakuApiKey(options.getResolvedConfig);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -72,7 +72,7 @@ export async function jimakuFetchJsonService<T>(
|
||||
}
|
||||
|
||||
return jimakuFetchJsonRequest<T>(endpoint, query, {
|
||||
baseUrl: getJimakuBaseUrlService(
|
||||
baseUrl: getJimakuBaseUrl(
|
||||
options.getResolvedConfig,
|
||||
options.defaultBaseUrl,
|
||||
),
|
||||
@@ -134,7 +134,7 @@ function collectDictionaryFromPath(
|
||||
return terms;
|
||||
}
|
||||
|
||||
export async function createJlptVocabularyLookupService(
|
||||
export async function createJlptVocabularyLookup(
|
||||
options: JlptVocabLookupOptions,
|
||||
): Promise<(term: string) => JlptLevel | null> {
|
||||
const attemptedPaths: string[] = [];
|
||||
@@ -1,24 +1,24 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
copyCurrentSubtitleService,
|
||||
handleMineSentenceDigitService,
|
||||
handleMultiCopyDigitService,
|
||||
mineSentenceCardService,
|
||||
} from "./mining-service";
|
||||
copyCurrentSubtitle,
|
||||
handleMineSentenceDigit,
|
||||
handleMultiCopyDigit,
|
||||
mineSentenceCard,
|
||||
} from "./mining";
|
||||
|
||||
test("copyCurrentSubtitleService reports tracker and subtitle guards", () => {
|
||||
test("copyCurrentSubtitle reports tracker and subtitle guards", () => {
|
||||
const osd: string[] = [];
|
||||
const copied: string[] = [];
|
||||
|
||||
copyCurrentSubtitleService({
|
||||
copyCurrentSubtitle({
|
||||
subtitleTimingTracker: null,
|
||||
writeClipboardText: (text) => copied.push(text),
|
||||
showMpvOsd: (text) => osd.push(text),
|
||||
});
|
||||
assert.equal(osd.at(-1), "Subtitle tracker not available");
|
||||
|
||||
copyCurrentSubtitleService({
|
||||
copyCurrentSubtitle({
|
||||
subtitleTimingTracker: {
|
||||
getRecentBlocks: () => [],
|
||||
getCurrentSubtitle: () => null,
|
||||
@@ -31,11 +31,11 @@ test("copyCurrentSubtitleService reports tracker and subtitle guards", () => {
|
||||
assert.deepEqual(copied, []);
|
||||
});
|
||||
|
||||
test("copyCurrentSubtitleService copies current subtitle text", () => {
|
||||
test("copyCurrentSubtitle copies current subtitle text", () => {
|
||||
const osd: string[] = [];
|
||||
const copied: string[] = [];
|
||||
|
||||
copyCurrentSubtitleService({
|
||||
copyCurrentSubtitle({
|
||||
subtitleTimingTracker: {
|
||||
getRecentBlocks: () => [],
|
||||
getCurrentSubtitle: () => "hello world",
|
||||
@@ -49,11 +49,11 @@ test("copyCurrentSubtitleService copies current subtitle text", () => {
|
||||
assert.equal(osd.at(-1), "Copied subtitle");
|
||||
});
|
||||
|
||||
test("mineSentenceCardService handles missing integration and disconnected mpv", async () => {
|
||||
test("mineSentenceCard handles missing integration and disconnected mpv", async () => {
|
||||
const osd: string[] = [];
|
||||
|
||||
assert.equal(
|
||||
await mineSentenceCardService({
|
||||
await mineSentenceCard({
|
||||
ankiIntegration: null,
|
||||
mpvClient: null,
|
||||
showMpvOsd: (text) => osd.push(text),
|
||||
@@ -62,8 +62,8 @@ test("mineSentenceCardService handles missing integration and disconnected mpv",
|
||||
);
|
||||
assert.equal(osd.at(-1), "AnkiConnect integration not enabled");
|
||||
|
||||
assert.equal(
|
||||
await mineSentenceCardService({
|
||||
assert.equal(
|
||||
await mineSentenceCard({
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
@@ -84,7 +84,7 @@ test("mineSentenceCardService handles missing integration and disconnected mpv",
|
||||
assert.equal(osd.at(-1), "MPV not connected");
|
||||
});
|
||||
|
||||
test("mineSentenceCardService creates sentence card from mpv subtitle state", async () => {
|
||||
test("mineSentenceCard creates sentence card from mpv subtitle state", async () => {
|
||||
const created: Array<{
|
||||
sentence: string;
|
||||
startTime: number;
|
||||
@@ -92,7 +92,7 @@ test("mineSentenceCardService creates sentence card from mpv subtitle state", as
|
||||
secondarySub?: string;
|
||||
}> = [];
|
||||
|
||||
const createdCard = await mineSentenceCardService({
|
||||
const createdCard = await mineSentenceCard({
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
@@ -123,11 +123,11 @@ test("mineSentenceCardService creates sentence card from mpv subtitle state", as
|
||||
]);
|
||||
});
|
||||
|
||||
test("handleMultiCopyDigitService copies available history and reports truncation", () => {
|
||||
test("handleMultiCopyDigit copies available history and reports truncation", () => {
|
||||
const osd: string[] = [];
|
||||
const copied: string[] = [];
|
||||
|
||||
handleMultiCopyDigitService(5, {
|
||||
handleMultiCopyDigit(5, {
|
||||
subtitleTimingTracker: {
|
||||
getRecentBlocks: (count) => ["a", "b"].slice(0, count),
|
||||
getCurrentSubtitle: () => null,
|
||||
@@ -141,12 +141,12 @@ test("handleMultiCopyDigitService copies available history and reports truncatio
|
||||
assert.equal(osd.at(-1), "Only 2 lines available, copied 2");
|
||||
});
|
||||
|
||||
test("handleMineSentenceDigitService reports async create failures", async () => {
|
||||
test("handleMineSentenceDigit reports async create failures", async () => {
|
||||
const osd: string[] = [];
|
||||
const logs: Array<{ message: string; err: unknown }> = [];
|
||||
let cardsMined = 0;
|
||||
|
||||
handleMineSentenceDigitService(2, {
|
||||
handleMineSentenceDigit(2, {
|
||||
subtitleTimingTracker: {
|
||||
getRecentBlocks: () => ["one", "two"],
|
||||
getCurrentSubtitle: () => null,
|
||||
@@ -184,7 +184,7 @@ test("handleMineSentenceDigitService increments successful card count", async ()
|
||||
const osd: string[] = [];
|
||||
let cardsMined = 0;
|
||||
|
||||
handleMineSentenceDigitService(2, {
|
||||
handleMineSentenceDigit(2, {
|
||||
subtitleTimingTracker: {
|
||||
getRecentBlocks: () => ["one", "two"],
|
||||
getCurrentSubtitle: () => null,
|
||||
@@ -24,7 +24,7 @@ interface MpvClientLike {
|
||||
currentSecondarySubText?: string;
|
||||
}
|
||||
|
||||
export function handleMultiCopyDigitService(
|
||||
export function handleMultiCopyDigit(
|
||||
count: number,
|
||||
deps: {
|
||||
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
|
||||
@@ -50,7 +50,7 @@ export function handleMultiCopyDigitService(
|
||||
}
|
||||
}
|
||||
|
||||
export function copyCurrentSubtitleService(deps: {
|
||||
export function copyCurrentSubtitle(deps: {
|
||||
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
|
||||
writeClipboardText: (text: string) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
@@ -79,7 +79,7 @@ function requireAnkiIntegration(
|
||||
return ankiIntegration;
|
||||
}
|
||||
|
||||
export async function updateLastCardFromClipboardService(deps: {
|
||||
export async function updateLastCardFromClipboard(deps: {
|
||||
ankiIntegration: AnkiIntegrationLike | null;
|
||||
readClipboardText: () => string;
|
||||
showMpvOsd: (text: string) => void;
|
||||
@@ -89,7 +89,7 @@ export async function updateLastCardFromClipboardService(deps: {
|
||||
await anki.updateLastAddedFromClipboard(deps.readClipboardText());
|
||||
}
|
||||
|
||||
export async function triggerFieldGroupingService(deps: {
|
||||
export async function triggerFieldGrouping(deps: {
|
||||
ankiIntegration: AnkiIntegrationLike | null;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}): Promise<void> {
|
||||
@@ -98,7 +98,7 @@ export async function triggerFieldGroupingService(deps: {
|
||||
await anki.triggerFieldGroupingForLastAddedCard();
|
||||
}
|
||||
|
||||
export async function markLastCardAsAudioCardService(deps: {
|
||||
export async function markLastCardAsAudioCard(deps: {
|
||||
ankiIntegration: AnkiIntegrationLike | null;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}): Promise<void> {
|
||||
@@ -107,7 +107,7 @@ export async function markLastCardAsAudioCardService(deps: {
|
||||
await anki.markLastCardAsAudioCard();
|
||||
}
|
||||
|
||||
export async function mineSentenceCardService(deps: {
|
||||
export async function mineSentenceCard(deps: {
|
||||
ankiIntegration: AnkiIntegrationLike | null;
|
||||
mpvClient: MpvClientLike | null;
|
||||
showMpvOsd: (text: string) => void;
|
||||
@@ -133,7 +133,7 @@ export async function mineSentenceCardService(deps: {
|
||||
);
|
||||
}
|
||||
|
||||
export function handleMineSentenceDigitService(
|
||||
export function handleMineSentenceDigit(
|
||||
count: number,
|
||||
deps: {
|
||||
subtitleTimingTracker: SubtitleTimingTrackerLike | null;
|
||||
@@ -1,16 +1,16 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
playNextSubtitleRuntimeService,
|
||||
replayCurrentSubtitleRuntimeService,
|
||||
sendMpvCommandRuntimeService,
|
||||
setMpvSubVisibilityRuntimeService,
|
||||
showMpvOsdRuntimeService,
|
||||
} from "./mpv-service";
|
||||
playNextSubtitleRuntime,
|
||||
replayCurrentSubtitleRuntime,
|
||||
sendMpvCommandRuntime,
|
||||
setMpvSubVisibilityRuntime,
|
||||
showMpvOsdRuntime,
|
||||
} from "./mpv";
|
||||
|
||||
test("showMpvOsdRuntimeService sends show-text when connected", () => {
|
||||
test("showMpvOsdRuntime sends show-text when connected", () => {
|
||||
const commands: (string | number)[][] = [];
|
||||
showMpvOsdRuntimeService(
|
||||
showMpvOsdRuntime(
|
||||
{
|
||||
connected: true,
|
||||
send: ({ command }) => {
|
||||
@@ -22,9 +22,9 @@ test("showMpvOsdRuntimeService sends show-text when connected", () => {
|
||||
assert.deepEqual(commands, [["show-text", "hello", "3000"]]);
|
||||
});
|
||||
|
||||
test("showMpvOsdRuntimeService logs fallback when disconnected", () => {
|
||||
test("showMpvOsdRuntime logs fallback when disconnected", () => {
|
||||
const logs: string[] = [];
|
||||
showMpvOsdRuntimeService(
|
||||
showMpvOsdRuntime(
|
||||
{
|
||||
connected: false,
|
||||
send: () => {},
|
||||
@@ -55,10 +55,10 @@ test("mpv runtime command wrappers call expected client methods", () => {
|
||||
},
|
||||
};
|
||||
|
||||
replayCurrentSubtitleRuntimeService(client);
|
||||
playNextSubtitleRuntimeService(client);
|
||||
sendMpvCommandRuntimeService(client, ["script-message", "x"]);
|
||||
setMpvSubVisibilityRuntimeService(client, false);
|
||||
replayCurrentSubtitleRuntime(client);
|
||||
playNextSubtitleRuntime(client);
|
||||
sendMpvCommandRuntime(client, ["script-message", "x"]);
|
||||
setMpvSubVisibilityRuntime(client, false);
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"replay",
|
||||
@@ -1,25 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { MpvSubtitleRenderMetrics } from "../../types";
|
||||
import {
|
||||
applyMpvSubtitleRenderMetricsPatchService,
|
||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
} from "./mpv-render-metrics-service";
|
||||
|
||||
const BASE: MpvSubtitleRenderMetrics = {
|
||||
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
};
|
||||
|
||||
test("applyMpvSubtitleRenderMetricsPatchService returns unchanged on empty patch", () => {
|
||||
const { next, changed } = applyMpvSubtitleRenderMetricsPatchService(BASE, {});
|
||||
assert.equal(changed, false);
|
||||
assert.deepEqual(next, BASE);
|
||||
});
|
||||
|
||||
test("applyMpvSubtitleRenderMetricsPatchService reports changed when patch modifies value", () => {
|
||||
const { next, changed } = applyMpvSubtitleRenderMetricsPatchService(BASE, {
|
||||
subPos: 95,
|
||||
});
|
||||
assert.equal(changed, true);
|
||||
assert.equal(next.subPos, 95);
|
||||
});
|
||||
25
src/core/services/mpv-render-metrics.test.ts
Normal file
25
src/core/services/mpv-render-metrics.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { MpvSubtitleRenderMetrics } from "../../types";
|
||||
import {
|
||||
applyMpvSubtitleRenderMetricsPatch,
|
||||
DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
} from "./mpv-render-metrics";
|
||||
|
||||
const BASE: MpvSubtitleRenderMetrics = {
|
||||
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||
};
|
||||
|
||||
test("applyMpvSubtitleRenderMetricsPatch returns unchanged on empty patch", () => {
|
||||
const { next, changed } = applyMpvSubtitleRenderMetricsPatch(BASE, {});
|
||||
assert.equal(changed, false);
|
||||
assert.deepEqual(next, BASE);
|
||||
});
|
||||
|
||||
test("applyMpvSubtitleRenderMetricsPatch reports changed when patch modifies value", () => {
|
||||
const { next, changed } = applyMpvSubtitleRenderMetricsPatch(BASE, {
|
||||
subPos: 95,
|
||||
});
|
||||
assert.equal(changed, true);
|
||||
assert.equal(next.subPos, 95);
|
||||
});
|
||||
@@ -25,10 +25,10 @@ export function sanitizeMpvSubtitleRenderMetrics(
|
||||
patch: Partial<MpvSubtitleRenderMetrics> | null | undefined,
|
||||
): MpvSubtitleRenderMetrics {
|
||||
if (!patch) return current;
|
||||
return updateMpvSubtitleRenderMetricsService(current, patch);
|
||||
return updateMpvSubtitleRenderMetrics(current, patch);
|
||||
}
|
||||
|
||||
export function updateMpvSubtitleRenderMetricsService(
|
||||
export function updateMpvSubtitleRenderMetrics(
|
||||
current: MpvSubtitleRenderMetrics,
|
||||
patch: Partial<MpvSubtitleRenderMetrics>,
|
||||
): MpvSubtitleRenderMetrics {
|
||||
@@ -83,11 +83,11 @@ export function updateMpvSubtitleRenderMetricsService(
|
||||
};
|
||||
}
|
||||
|
||||
export function applyMpvSubtitleRenderMetricsPatchService(
|
||||
export function applyMpvSubtitleRenderMetricsPatch(
|
||||
current: MpvSubtitleRenderMetrics,
|
||||
patch: Partial<MpvSubtitleRenderMetrics>,
|
||||
): { next: MpvSubtitleRenderMetrics; changed: boolean } {
|
||||
const next = updateMpvSubtitleRenderMetricsService(current, patch);
|
||||
const next = updateMpvSubtitleRenderMetrics(current, patch);
|
||||
const changed =
|
||||
next.subPos !== current.subPos ||
|
||||
next.subFontSize !== current.subFontSize ||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv-service";
|
||||
import { resolveCurrentAudioStreamIndex } from "./mpv";
|
||||
|
||||
test("resolveCurrentAudioStreamIndex returns selected ff-index when no current track id", () => {
|
||||
assert.equal(
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
MpvIpcClientDeps,
|
||||
MpvIpcClientProtocolDeps,
|
||||
MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY,
|
||||
} from "./mpv-service";
|
||||
} from "./mpv";
|
||||
import { MPV_REQUEST_ID_TRACK_LIST_AUDIO } from "./mpv-protocol";
|
||||
|
||||
function makeDeps(
|
||||
@@ -55,7 +55,7 @@ export interface MpvRuntimeClientLike {
|
||||
setSubVisibility?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export function showMpvOsdRuntimeService(
|
||||
export function showMpvOsdRuntime(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
text: string,
|
||||
fallbackLog: (text: string) => void = (line) => logger.info(line),
|
||||
@@ -67,21 +67,21 @@ export function showMpvOsdRuntimeService(
|
||||
fallbackLog(`OSD (MPV not connected): ${text}`);
|
||||
}
|
||||
|
||||
export function replayCurrentSubtitleRuntimeService(
|
||||
export function replayCurrentSubtitleRuntime(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.replayCurrentSubtitle) return;
|
||||
mpvClient.replayCurrentSubtitle();
|
||||
}
|
||||
|
||||
export function playNextSubtitleRuntimeService(
|
||||
export function playNextSubtitleRuntime(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
): void {
|
||||
if (!mpvClient?.playNextSubtitle) return;
|
||||
mpvClient.playNextSubtitle();
|
||||
}
|
||||
|
||||
export function sendMpvCommandRuntimeService(
|
||||
export function sendMpvCommandRuntime(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
command: (string | number)[],
|
||||
): void {
|
||||
@@ -89,7 +89,7 @@ export function sendMpvCommandRuntimeService(
|
||||
mpvClient.send({ command });
|
||||
}
|
||||
|
||||
export function setMpvSubVisibilityRuntimeService(
|
||||
export function setMpvSubVisibilityRuntime(
|
||||
mpvClient: MpvRuntimeClientLike | null,
|
||||
visible: boolean,
|
||||
): void {
|
||||
@@ -1,17 +1,17 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createNumericShortcutRuntimeService,
|
||||
createNumericShortcutSessionService,
|
||||
} from "./numeric-shortcut-service";
|
||||
createNumericShortcutRuntime,
|
||||
createNumericShortcutSession,
|
||||
} from "./numeric-shortcut";
|
||||
|
||||
test("createNumericShortcutRuntimeService creates sessions wired to globalShortcut", () => {
|
||||
test("createNumericShortcutRuntime creates sessions wired to globalShortcut", () => {
|
||||
const registered: string[] = [];
|
||||
const unregistered: string[] = [];
|
||||
const osd: string[] = [];
|
||||
const handlers = new Map<string, () => void>();
|
||||
|
||||
const runtime = createNumericShortcutRuntimeService({
|
||||
const runtime = createNumericShortcutRuntime({
|
||||
globalShortcut: {
|
||||
register: (accelerator, callback) => {
|
||||
registered.push(accelerator);
|
||||
@@ -54,7 +54,7 @@ test("numeric shortcut session handles digit selection and unregisters shortcuts
|
||||
const handlers = new Map<string, () => void>();
|
||||
const unregistered: string[] = [];
|
||||
const osd: string[] = [];
|
||||
const session = createNumericShortcutSessionService({
|
||||
const session = createNumericShortcutSession({
|
||||
registerShortcut: (accelerator, handler) => {
|
||||
handlers.set(accelerator, handler);
|
||||
return true;
|
||||
@@ -96,7 +96,7 @@ test("numeric shortcut session handles digit selection and unregisters shortcuts
|
||||
|
||||
test("numeric shortcut session emits timeout message", () => {
|
||||
const osd: string[] = [];
|
||||
const session = createNumericShortcutSessionService({
|
||||
const session = createNumericShortcutSession({
|
||||
registerShortcut: () => true,
|
||||
unregisterShortcut: () => {},
|
||||
setTimer: (handler) => {
|
||||
@@ -126,7 +126,7 @@ test("numeric shortcut session emits timeout message", () => {
|
||||
test("numeric shortcut session handles escape cancellation", () => {
|
||||
const handlers = new Map<string, () => void>();
|
||||
const osd: string[] = [];
|
||||
const session = createNumericShortcutSessionService({
|
||||
const session = createNumericShortcutSession({
|
||||
registerShortcut: (accelerator, handler) => {
|
||||
handlers.set(accelerator, handler);
|
||||
return true;
|
||||
@@ -13,11 +13,11 @@ export interface NumericShortcutRuntimeOptions {
|
||||
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
|
||||
}
|
||||
|
||||
export function createNumericShortcutRuntimeService(
|
||||
export function createNumericShortcutRuntime(
|
||||
options: NumericShortcutRuntimeOptions,
|
||||
) {
|
||||
const createSession = () =>
|
||||
createNumericShortcutSessionService({
|
||||
createNumericShortcutSession({
|
||||
registerShortcut: (accelerator, handler) =>
|
||||
options.globalShortcut.register(accelerator, handler),
|
||||
unregisterShortcut: (accelerator) =>
|
||||
@@ -52,7 +52,7 @@ export interface NumericShortcutSessionStartParams {
|
||||
messages: NumericShortcutSessionMessages;
|
||||
}
|
||||
|
||||
export function createNumericShortcutSessionService(
|
||||
export function createNumericShortcutSession(
|
||||
deps: NumericShortcutSessionDeps,
|
||||
) {
|
||||
let active = false;
|
||||
@@ -2,16 +2,16 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { KikuFieldGroupingChoice } from "../../types";
|
||||
import {
|
||||
createFieldGroupingCallbackRuntimeService,
|
||||
sendToVisibleOverlayRuntimeService,
|
||||
} from "./overlay-bridge-service";
|
||||
createFieldGroupingCallbackRuntime,
|
||||
sendToVisibleOverlayRuntime,
|
||||
} from "./overlay-bridge";
|
||||
|
||||
test("sendToVisibleOverlayRuntimeService restores visibility flag when opening hidden overlay modal", () => {
|
||||
test("sendToVisibleOverlayRuntime restores visibility flag when opening hidden overlay modal", () => {
|
||||
const sent: unknown[][] = [];
|
||||
const restoreSet = new Set<"runtime-options" | "subsync">();
|
||||
let visibleOverlayVisible = false;
|
||||
|
||||
const ok = sendToVisibleOverlayRuntimeService({
|
||||
const ok = sendToVisibleOverlayRuntime({
|
||||
mainWindow: {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
@@ -36,9 +36,9 @@ test("sendToVisibleOverlayRuntimeService restores visibility flag when opening h
|
||||
assert.deepEqual(sent, [["runtime-options:open"]]);
|
||||
});
|
||||
|
||||
test("createFieldGroupingCallbackRuntimeService cancels when overlay request cannot be sent", async () => {
|
||||
test("createFieldGroupingCallbackRuntime cancels when overlay request cannot be sent", async () => {
|
||||
let resolver: ((choice: KikuFieldGroupingChoice) => void) | null = null;
|
||||
const callback = createFieldGroupingCallbackRuntimeService<
|
||||
const callback = createFieldGroupingCallbackRuntime<
|
||||
"runtime-options" | "subsync"
|
||||
>({
|
||||
getVisibleOverlayVisible: () => false,
|
||||
@@ -2,10 +2,10 @@ import {
|
||||
KikuFieldGroupingChoice,
|
||||
KikuFieldGroupingRequestData,
|
||||
} from "../../types";
|
||||
import { createFieldGroupingCallbackService } from "./field-grouping-service";
|
||||
import { createFieldGroupingCallback } from "./field-grouping";
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
export function sendToVisibleOverlayRuntimeService<T extends string>(options: {
|
||||
export function sendToVisibleOverlayRuntime<T extends string>(options: {
|
||||
mainWindow: BrowserWindow | null;
|
||||
visibleOverlayVisible: boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
@@ -45,7 +45,7 @@ export function sendToVisibleOverlayRuntimeService<T extends string>(options: {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function createFieldGroupingCallbackRuntimeService<T extends string>(
|
||||
export function createFieldGroupingCallbackRuntime<T extends string>(
|
||||
options: {
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
@@ -62,7 +62,7 @@ export function createFieldGroupingCallbackRuntimeService<T extends string>(
|
||||
) => boolean;
|
||||
},
|
||||
): (data: KikuFieldGroupingRequestData) => Promise<KikuFieldGroupingChoice> {
|
||||
return createFieldGroupingCallbackService({
|
||||
return createFieldGroupingCallback({
|
||||
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
|
||||
setVisibleOverlayVisible: options.setVisibleOverlayVisible,
|
||||
@@ -2,9 +2,9 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
createOverlayContentMeasurementStoreService,
|
||||
createOverlayContentMeasurementStore,
|
||||
sanitizeOverlayContentMeasurement,
|
||||
} from "./overlay-content-measurement-service";
|
||||
} from "./overlay-content-measurement";
|
||||
|
||||
test("sanitizeOverlayContentMeasurement accepts valid payload with null rect", () => {
|
||||
const measurement = sanitizeOverlayContentMeasurement(
|
||||
@@ -40,7 +40,7 @@ test("sanitizeOverlayContentMeasurement rejects invalid ranges", () => {
|
||||
});
|
||||
|
||||
test("overlay measurement store keeps latest payload per layer", () => {
|
||||
const store = createOverlayContentMeasurementStoreService({
|
||||
const store = createOverlayContentMeasurementStore({
|
||||
now: () => 1000,
|
||||
warn: () => {
|
||||
// noop
|
||||
@@ -69,7 +69,7 @@ test("overlay measurement store keeps latest payload per layer", () => {
|
||||
test("overlay measurement store rate-limits invalid payload warnings", () => {
|
||||
let now = 1_000;
|
||||
const warnings: string[] = [];
|
||||
const store = createOverlayContentMeasurementStoreService({
|
||||
const store = createOverlayContentMeasurementStore({
|
||||
now: () => now,
|
||||
warn: (message) => {
|
||||
warnings.push(message);
|
||||
@@ -105,7 +105,7 @@ function readFiniteInRange(
|
||||
return value;
|
||||
}
|
||||
|
||||
export function createOverlayContentMeasurementStoreService(options?: {
|
||||
export function createOverlayContentMeasurementStore(options?: {
|
||||
now?: () => number;
|
||||
warn?: (message: string) => void;
|
||||
}) {
|
||||
@@ -1,13 +1,13 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
createOverlayManagerService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./overlay-manager-service";
|
||||
broadcastRuntimeOptionsChangedRuntime,
|
||||
createOverlayManager,
|
||||
setOverlayDebugVisualizationEnabledRuntime,
|
||||
} from "./overlay-manager";
|
||||
|
||||
test("overlay manager initializes with empty windows and hidden overlays", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const manager = createOverlayManager();
|
||||
assert.equal(manager.getMainWindow(), null);
|
||||
assert.equal(manager.getInvisibleWindow(), null);
|
||||
assert.equal(manager.getVisibleOverlayVisible(), false);
|
||||
@@ -16,7 +16,7 @@ test("overlay manager initializes with empty windows and hidden overlays", () =>
|
||||
});
|
||||
|
||||
test("overlay manager stores window references and returns stable window order", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const manager = createOverlayManager();
|
||||
const visibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
|
||||
const invisibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
|
||||
|
||||
@@ -31,7 +31,7 @@ test("overlay manager stores window references and returns stable window order",
|
||||
});
|
||||
|
||||
test("overlay manager excludes destroyed windows", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const manager = createOverlayManager();
|
||||
manager.setMainWindow({ isDestroyed: () => true } as unknown as Electron.BrowserWindow);
|
||||
manager.setInvisibleWindow({ isDestroyed: () => false } as unknown as Electron.BrowserWindow);
|
||||
|
||||
@@ -39,7 +39,7 @@ test("overlay manager excludes destroyed windows", () => {
|
||||
});
|
||||
|
||||
test("overlay manager stores visibility state", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const manager = createOverlayManager();
|
||||
|
||||
manager.setVisibleOverlayVisible(true);
|
||||
manager.setInvisibleOverlayVisible(true);
|
||||
@@ -48,7 +48,7 @@ test("overlay manager stores visibility state", () => {
|
||||
});
|
||||
|
||||
test("overlay manager broadcasts to non-destroyed windows", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const manager = createOverlayManager();
|
||||
const calls: unknown[][] = [];
|
||||
const aliveWindow = {
|
||||
isDestroyed: () => false,
|
||||
@@ -73,7 +73,7 @@ test("overlay manager broadcasts to non-destroyed windows", () => {
|
||||
});
|
||||
|
||||
test("overlay manager applies bounds by layer", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const manager = createOverlayManager();
|
||||
const visibleCalls: Electron.Rectangle[] = [];
|
||||
const invisibleCalls: Electron.Rectangle[] = [];
|
||||
const visibleWindow = {
|
||||
@@ -110,14 +110,14 @@ test("overlay manager applies bounds by layer", () => {
|
||||
|
||||
test("runtime-option and debug broadcasts use expected channels", () => {
|
||||
const broadcasts: unknown[][] = [];
|
||||
broadcastRuntimeOptionsChangedRuntimeService(
|
||||
broadcastRuntimeOptionsChangedRuntime(
|
||||
() => [],
|
||||
(channel, ...args) => {
|
||||
broadcasts.push([channel, ...args]);
|
||||
},
|
||||
);
|
||||
let state = false;
|
||||
const changed = setOverlayDebugVisualizationEnabledRuntimeService(
|
||||
const changed = setOverlayDebugVisualizationEnabledRuntime(
|
||||
state,
|
||||
true,
|
||||
(enabled) => {
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { RuntimeOptionState, WindowGeometry } from "../../types";
|
||||
import { updateOverlayWindowBoundsService } from "./overlay-window-service";
|
||||
import { updateOverlayWindowBounds } from "./overlay-window";
|
||||
|
||||
type OverlayLayer = "visible" | "invisible";
|
||||
|
||||
export interface OverlayManagerService {
|
||||
export interface OverlayManager {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
setMainWindow: (window: BrowserWindow | null) => void;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
@@ -19,7 +19,7 @@ export interface OverlayManagerService {
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
export function createOverlayManagerService(): OverlayManagerService {
|
||||
export function createOverlayManager(): OverlayManager {
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let invisibleWindow: BrowserWindow | null = null;
|
||||
let visibleOverlayVisible = false;
|
||||
@@ -37,7 +37,7 @@ export function createOverlayManagerService(): OverlayManagerService {
|
||||
getOverlayWindow: (layer) =>
|
||||
layer === "visible" ? mainWindow : invisibleWindow,
|
||||
setOverlayWindowBounds: (layer, geometry) => {
|
||||
updateOverlayWindowBoundsService(
|
||||
updateOverlayWindowBounds(
|
||||
geometry,
|
||||
layer === "visible" ? mainWindow : invisibleWindow,
|
||||
);
|
||||
@@ -75,14 +75,14 @@ export function createOverlayManagerService(): OverlayManagerService {
|
||||
};
|
||||
}
|
||||
|
||||
export function broadcastRuntimeOptionsChangedRuntimeService(
|
||||
export function broadcastRuntimeOptionsChangedRuntime(
|
||||
getRuntimeOptionsState: () => RuntimeOptionState[],
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||
): void {
|
||||
broadcastToOverlayWindows("runtime-options:changed", getRuntimeOptionsState());
|
||||
}
|
||||
|
||||
export function setOverlayDebugVisualizationEnabledRuntimeService(
|
||||
export function setOverlayDebugVisualizationEnabledRuntime(
|
||||
currentEnabled: boolean,
|
||||
nextEnabled: boolean,
|
||||
setState: (enabled: boolean) => void,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
WindowGeometry,
|
||||
} from "../../types";
|
||||
|
||||
export function initializeOverlayRuntimeService(options: {
|
||||
export function initializeOverlayRuntime(options: {
|
||||
backendOverride: string | null;
|
||||
getInitialInvisibleOverlayVisibility: () => boolean;
|
||||
createMainWindow: () => void;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { OverlayShortcutHandlers } from "./overlay-shortcut-service";
|
||||
import { OverlayShortcutHandlers } from "./overlay-shortcut";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-shortcut-handler");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback-service";
|
||||
import { isGlobalShortcutRegisteredSafe } from "./shortcut-fallback";
|
||||
import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:overlay-shortcut-service");
|
||||
@@ -26,7 +26,7 @@ export interface OverlayShortcutLifecycleDeps {
|
||||
cancelPendingMineSentenceMultiple: () => void;
|
||||
}
|
||||
|
||||
export function registerOverlayShortcutsService(
|
||||
export function registerOverlayShortcuts(
|
||||
shortcuts: ConfiguredShortcuts,
|
||||
handlers: OverlayShortcutHandlers,
|
||||
): boolean {
|
||||
@@ -140,7 +140,7 @@ export function registerOverlayShortcutsService(
|
||||
return registeredAny;
|
||||
}
|
||||
|
||||
export function unregisterOverlayShortcutsService(
|
||||
export function unregisterOverlayShortcuts(
|
||||
shortcuts: ConfiguredShortcuts,
|
||||
): void {
|
||||
if (shortcuts.copySubtitle) {
|
||||
@@ -178,45 +178,45 @@ export function unregisterOverlayShortcutsService(
|
||||
}
|
||||
}
|
||||
|
||||
export function registerOverlayShortcutsRuntimeService(
|
||||
export function registerOverlayShortcutsRuntime(
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
return registerOverlayShortcutsService(
|
||||
return registerOverlayShortcuts(
|
||||
deps.getConfiguredShortcuts(),
|
||||
deps.getOverlayHandlers(),
|
||||
);
|
||||
}
|
||||
|
||||
export function unregisterOverlayShortcutsRuntimeService(
|
||||
export function unregisterOverlayShortcutsRuntime(
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
if (!shortcutsRegistered) return shortcutsRegistered;
|
||||
deps.cancelPendingMultiCopy();
|
||||
deps.cancelPendingMineSentenceMultiple();
|
||||
unregisterOverlayShortcutsService(deps.getConfiguredShortcuts());
|
||||
unregisterOverlayShortcuts(deps.getConfiguredShortcuts());
|
||||
return false;
|
||||
}
|
||||
|
||||
export function syncOverlayShortcutsRuntimeService(
|
||||
export function syncOverlayShortcutsRuntime(
|
||||
shouldBeActive: boolean,
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
if (shouldBeActive) {
|
||||
return registerOverlayShortcutsRuntimeService(deps);
|
||||
return registerOverlayShortcutsRuntime(deps);
|
||||
}
|
||||
return unregisterOverlayShortcutsRuntimeService(shortcutsRegistered, deps);
|
||||
return unregisterOverlayShortcutsRuntime(shortcutsRegistered, deps);
|
||||
}
|
||||
|
||||
export function refreshOverlayShortcutsRuntimeService(
|
||||
export function refreshOverlayShortcutsRuntime(
|
||||
shouldBeActive: boolean,
|
||||
shortcutsRegistered: boolean,
|
||||
deps: OverlayShortcutLifecycleDeps,
|
||||
): boolean {
|
||||
const cleared = unregisterOverlayShortcutsRuntimeService(
|
||||
const cleared = unregisterOverlayShortcutsRuntime(
|
||||
shortcutsRegistered,
|
||||
deps,
|
||||
);
|
||||
return syncOverlayShortcutsRuntimeService(shouldBeActive, cleared, deps);
|
||||
return syncOverlayShortcutsRuntime(shouldBeActive, cleared, deps);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { BrowserWindow, screen } from "electron";
|
||||
import { BaseWindowTracker } from "../../window-trackers";
|
||||
import { WindowGeometry } from "../../types";
|
||||
|
||||
export function updateVisibleOverlayVisibilityService(args: {
|
||||
export function updateVisibleOverlayVisibility(args: {
|
||||
visibleOverlayVisible: boolean;
|
||||
mainWindow: BrowserWindow | null;
|
||||
windowTracker: BaseWindowTracker | null;
|
||||
@@ -66,7 +66,7 @@ export function updateVisibleOverlayVisibilityService(args: {
|
||||
args.syncOverlayShortcuts();
|
||||
}
|
||||
|
||||
export function updateInvisibleOverlayVisibilityService(args: {
|
||||
export function updateInvisibleOverlayVisibility(args: {
|
||||
invisibleWindow: BrowserWindow | null;
|
||||
visibleOverlayVisible: boolean;
|
||||
invisibleOverlayVisible: boolean;
|
||||
@@ -131,7 +131,7 @@ export function updateInvisibleOverlayVisibilityService(args: {
|
||||
args.syncOverlayShortcuts();
|
||||
}
|
||||
|
||||
export function syncInvisibleOverlayMousePassthroughService(options: {
|
||||
export function syncInvisibleOverlayMousePassthrough(options: {
|
||||
hasInvisibleWindow: () => boolean;
|
||||
setIgnoreMouseEvents: (ignore: boolean, extra?: { forward: boolean }) => void;
|
||||
visibleOverlayVisible: boolean;
|
||||
@@ -145,7 +145,7 @@ export function syncInvisibleOverlayMousePassthroughService(options: {
|
||||
}
|
||||
}
|
||||
|
||||
export function setVisibleOverlayVisibleService(options: {
|
||||
export function setVisibleOverlayVisible(options: {
|
||||
visible: boolean;
|
||||
setVisibleOverlayVisibleState: (visible: boolean) => void;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
@@ -167,7 +167,7 @@ export function setVisibleOverlayVisibleService(options: {
|
||||
}
|
||||
}
|
||||
|
||||
export function setInvisibleOverlayVisibleService(options: {
|
||||
export function setInvisibleOverlayVisible(options: {
|
||||
visible: boolean;
|
||||
setInvisibleOverlayVisibleState: (visible: boolean) => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
@@ -7,7 +7,7 @@ const logger = createLogger("main:overlay-window");
|
||||
|
||||
export type OverlayWindowKind = "visible" | "invisible";
|
||||
|
||||
export function updateOverlayWindowBoundsService(
|
||||
export function updateOverlayWindowBounds(
|
||||
geometry: WindowGeometry,
|
||||
window: BrowserWindow | null,
|
||||
): void {
|
||||
@@ -20,7 +20,7 @@ export function updateOverlayWindowBoundsService(
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
|
||||
export function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
||||
if (process.platform === "darwin") {
|
||||
window.setAlwaysOnTop(true, "screen-saver", 1);
|
||||
window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||
@@ -30,7 +30,7 @@ export function ensureOverlayWindowLevelService(window: BrowserWindow): void {
|
||||
window.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
export function enforceOverlayLayerOrderService(options: {
|
||||
export function enforceOverlayLayerOrder(options: {
|
||||
visibleOverlayVisible: boolean;
|
||||
invisibleOverlayVisible: boolean;
|
||||
mainWindow: BrowserWindow | null;
|
||||
@@ -45,7 +45,7 @@ export function enforceOverlayLayerOrderService(options: {
|
||||
options.mainWindow.moveTop();
|
||||
}
|
||||
|
||||
export function createOverlayWindowService(
|
||||
export function createOverlayWindow(
|
||||
kind: OverlayWindowKind,
|
||||
options: {
|
||||
isDev: boolean;
|
||||
@@ -1,98 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService,
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService,
|
||||
} from "./startup-service";
|
||||
|
||||
const BASE_CONFIG = {
|
||||
auto_start_overlay: false,
|
||||
bind_visible_overlay_to_mpv_sub_visibility: true,
|
||||
invisibleOverlay: {
|
||||
startupVisibility: "platform-default" as const,
|
||||
},
|
||||
ankiConnect: {
|
||||
behavior: {
|
||||
autoUpdateNewCards: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("getInitialInvisibleOverlayVisibilityService handles visibility + platform", () => {
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibilityService(
|
||||
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "visible" } },
|
||||
"linux",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibilityService(
|
||||
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "hidden" } },
|
||||
"darwin",
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibilityService(BASE_CONFIG, "linux"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibilityService(BASE_CONFIG, "darwin"),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldAutoInitializeOverlayRuntimeFromConfigService respects auto start and visible startup", () => {
|
||||
assert.equal(
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService(BASE_CONFIG),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService({
|
||||
...BASE_CONFIG,
|
||||
auto_start_overlay: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigService({
|
||||
...BASE_CONFIG,
|
||||
invisibleOverlay: { startupVisibility: "visible" },
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldBindVisibleOverlayToMpvSubVisibilityService returns config value", () => {
|
||||
assert.equal(shouldBindVisibleOverlayToMpvSubVisibilityService(BASE_CONFIG), true);
|
||||
assert.equal(
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityService({
|
||||
...BASE_CONFIG,
|
||||
bind_visible_overlay_to_mpv_sub_visibility: false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("isAutoUpdateEnabledRuntimeService prefers runtime option and falls back to config", () => {
|
||||
assert.equal(
|
||||
isAutoUpdateEnabledRuntimeService(BASE_CONFIG, {
|
||||
getOptionValue: () => false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isAutoUpdateEnabledRuntimeService(
|
||||
{
|
||||
...BASE_CONFIG,
|
||||
ankiConnect: { behavior: { autoUpdateNewCards: false } },
|
||||
},
|
||||
null,
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(isAutoUpdateEnabledRuntimeService(BASE_CONFIG, null), true);
|
||||
});
|
||||
98
src/core/services/runtime-config.test.ts
Normal file
98
src/core/services/runtime-config.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
getInitialInvisibleOverlayVisibility,
|
||||
isAutoUpdateEnabledRuntime,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
shouldBindVisibleOverlayToMpvSubVisibility,
|
||||
} from "./startup";
|
||||
|
||||
const BASE_CONFIG = {
|
||||
auto_start_overlay: false,
|
||||
bind_visible_overlay_to_mpv_sub_visibility: true,
|
||||
invisibleOverlay: {
|
||||
startupVisibility: "platform-default" as const,
|
||||
},
|
||||
ankiConnect: {
|
||||
behavior: {
|
||||
autoUpdateNewCards: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("getInitialInvisibleOverlayVisibility handles visibility + platform", () => {
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibility(
|
||||
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "visible" } },
|
||||
"linux",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibility(
|
||||
{ ...BASE_CONFIG, invisibleOverlay: { startupVisibility: "hidden" } },
|
||||
"darwin",
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibility(BASE_CONFIG, "linux"),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
getInitialInvisibleOverlayVisibility(BASE_CONFIG, "darwin"),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldAutoInitializeOverlayRuntimeFromConfig respects auto start and visible startup", () => {
|
||||
assert.equal(
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig(BASE_CONFIG),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig({
|
||||
...BASE_CONFIG,
|
||||
auto_start_overlay: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig({
|
||||
...BASE_CONFIG,
|
||||
invisibleOverlay: { startupVisibility: "visible" },
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldBindVisibleOverlayToMpvSubVisibility returns config value", () => {
|
||||
assert.equal(shouldBindVisibleOverlayToMpvSubVisibility(BASE_CONFIG), true);
|
||||
assert.equal(
|
||||
shouldBindVisibleOverlayToMpvSubVisibility({
|
||||
...BASE_CONFIG,
|
||||
bind_visible_overlay_to_mpv_sub_visibility: false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("isAutoUpdateEnabledRuntime prefers runtime option and falls back to config", () => {
|
||||
assert.equal(
|
||||
isAutoUpdateEnabledRuntime(BASE_CONFIG, {
|
||||
getOptionValue: () => false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isAutoUpdateEnabledRuntime(
|
||||
{
|
||||
...BASE_CONFIG,
|
||||
ankiConnect: { behavior: { autoUpdateNewCards: false } },
|
||||
},
|
||||
null,
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(isAutoUpdateEnabledRuntime(BASE_CONFIG, null), true);
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
applyRuntimeOptionResultRuntimeService,
|
||||
cycleRuntimeOptionFromIpcRuntimeService,
|
||||
setRuntimeOptionFromIpcRuntimeService,
|
||||
} from "./runtime-options-ipc-service";
|
||||
applyRuntimeOptionResultRuntime,
|
||||
cycleRuntimeOptionFromIpcRuntime,
|
||||
setRuntimeOptionFromIpcRuntime,
|
||||
} from "./runtime-options-ipc";
|
||||
|
||||
test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
|
||||
test("applyRuntimeOptionResultRuntime emits success OSD message", () => {
|
||||
const osd: string[] = [];
|
||||
const result = applyRuntimeOptionResultRuntimeService(
|
||||
const result = applyRuntimeOptionResultRuntime(
|
||||
{ ok: true, osdMessage: "Updated" },
|
||||
(text) => {
|
||||
osd.push(text);
|
||||
@@ -19,9 +19,9 @@ test("applyRuntimeOptionResultRuntimeService emits success OSD message", () => {
|
||||
assert.deepEqual(osd, ["Updated"]);
|
||||
});
|
||||
|
||||
test("setRuntimeOptionFromIpcRuntimeService returns unavailable when manager missing", () => {
|
||||
test("setRuntimeOptionFromIpcRuntime returns unavailable when manager missing", () => {
|
||||
const osd: string[] = [];
|
||||
const result = setRuntimeOptionFromIpcRuntimeService(
|
||||
const result = setRuntimeOptionFromIpcRuntime(
|
||||
null,
|
||||
"anki.autoUpdateNewCards",
|
||||
true,
|
||||
@@ -34,9 +34,9 @@ test("setRuntimeOptionFromIpcRuntimeService returns unavailable when manager mis
|
||||
assert.deepEqual(osd, []);
|
||||
});
|
||||
|
||||
test("cycleRuntimeOptionFromIpcRuntimeService reports errors once", () => {
|
||||
test("cycleRuntimeOptionFromIpcRuntime reports errors once", () => {
|
||||
const osd: string[] = [];
|
||||
const result = cycleRuntimeOptionFromIpcRuntimeService(
|
||||
const result = cycleRuntimeOptionFromIpcRuntime(
|
||||
{
|
||||
setOptionValue: () => ({ ok: true }),
|
||||
cycleOption: () => ({ ok: false, error: "bad option" }),
|
||||
@@ -15,7 +15,7 @@ export interface RuntimeOptionsManagerLike {
|
||||
) => RuntimeOptionApplyResult;
|
||||
}
|
||||
|
||||
export function applyRuntimeOptionResultRuntimeService(
|
||||
export function applyRuntimeOptionResultRuntime(
|
||||
result: RuntimeOptionApplyResult,
|
||||
showMpvOsd: (text: string) => void,
|
||||
): RuntimeOptionApplyResult {
|
||||
@@ -25,7 +25,7 @@ export function applyRuntimeOptionResultRuntimeService(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function setRuntimeOptionFromIpcRuntimeService(
|
||||
export function setRuntimeOptionFromIpcRuntime(
|
||||
manager: RuntimeOptionsManagerLike | null,
|
||||
id: RuntimeOptionId,
|
||||
value: RuntimeOptionValue,
|
||||
@@ -34,7 +34,7 @@ export function setRuntimeOptionFromIpcRuntimeService(
|
||||
if (!manager) {
|
||||
return { ok: false, error: "Runtime options manager unavailable" };
|
||||
}
|
||||
const result = applyRuntimeOptionResultRuntimeService(
|
||||
const result = applyRuntimeOptionResultRuntime(
|
||||
manager.setOptionValue(id, value),
|
||||
showMpvOsd,
|
||||
);
|
||||
@@ -44,7 +44,7 @@ export function setRuntimeOptionFromIpcRuntimeService(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function cycleRuntimeOptionFromIpcRuntimeService(
|
||||
export function cycleRuntimeOptionFromIpcRuntime(
|
||||
manager: RuntimeOptionsManagerLike | null,
|
||||
id: RuntimeOptionId,
|
||||
direction: 1 | -1,
|
||||
@@ -53,7 +53,7 @@ export function cycleRuntimeOptionFromIpcRuntimeService(
|
||||
if (!manager) {
|
||||
return { ok: false, error: "Runtime options manager unavailable" };
|
||||
}
|
||||
const result = applyRuntimeOptionResultRuntimeService(
|
||||
const result = applyRuntimeOptionResultRuntime(
|
||||
manager.cycleOption(id, direction),
|
||||
showMpvOsd,
|
||||
);
|
||||
@@ -1,15 +1,15 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { SecondarySubMode } from "../../types";
|
||||
import { cycleSecondarySubModeService } from "./subtitle-position-service";
|
||||
import { cycleSecondarySubMode } from "./subtitle-position";
|
||||
|
||||
test("cycleSecondarySubModeService cycles and emits broadcast + OSD", () => {
|
||||
test("cycleSecondarySubMode cycles and emits broadcast + OSD", () => {
|
||||
let mode: SecondarySubMode = "hover";
|
||||
let lastToggleAt = 0;
|
||||
const broadcasts: SecondarySubMode[] = [];
|
||||
const osd: string[] = [];
|
||||
|
||||
cycleSecondarySubModeService({
|
||||
cycleSecondarySubMode({
|
||||
getSecondarySubMode: () => mode,
|
||||
setSecondarySubMode: (next) => {
|
||||
mode = next;
|
||||
@@ -33,13 +33,13 @@ test("cycleSecondarySubModeService cycles and emits broadcast + OSD", () => {
|
||||
assert.equal(lastToggleAt, 1000);
|
||||
});
|
||||
|
||||
test("cycleSecondarySubModeService obeys debounce window", () => {
|
||||
test("cycleSecondarySubMode obeys debounce window", () => {
|
||||
let mode: SecondarySubMode = "visible";
|
||||
let lastToggleAt = 950;
|
||||
let broadcasted = false;
|
||||
let osdShown = false;
|
||||
|
||||
cycleSecondarySubModeService({
|
||||
cycleSecondarySubMode({
|
||||
getSecondarySubMode: () => mode,
|
||||
setSecondarySubMode: (next) => {
|
||||
mode = next;
|
||||
@@ -19,7 +19,7 @@ export interface RegisterGlobalShortcutsServiceOptions {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
}
|
||||
|
||||
export function registerGlobalShortcutsService(
|
||||
export function registerGlobalShortcuts(
|
||||
options: RegisterGlobalShortcutsServiceOptions,
|
||||
): void {
|
||||
const visibleShortcut = options.shortcuts.toggleVisibleOverlayGlobal;
|
||||
@@ -1,8 +1,8 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
runStartupBootstrapRuntimeService,
|
||||
} from "./startup-service";
|
||||
runStartupBootstrapRuntime,
|
||||
} from "./startup";
|
||||
import { CliArgs } from "../../cli/args";
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
@@ -40,7 +40,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
};
|
||||
}
|
||||
|
||||
test("runStartupBootstrapRuntimeService configures startup state and starts lifecycle", () => {
|
||||
test("runStartupBootstrapRuntime configures startup state and starts lifecycle", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({
|
||||
logLevel: "debug",
|
||||
@@ -51,7 +51,7 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
|
||||
texthooker: true,
|
||||
});
|
||||
|
||||
const result = runStartupBootstrapRuntimeService({
|
||||
const result = runStartupBootstrapRuntime({
|
||||
argv: ["node", "main.ts", "--log-level", "debug"],
|
||||
parseArgs: () => args,
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
@@ -77,13 +77,13 @@ test("runStartupBootstrapRuntimeService configures startup state and starts life
|
||||
]);
|
||||
});
|
||||
|
||||
test("runStartupBootstrapRuntimeService keeps log-level precedence for repeated calls", () => {
|
||||
test("runStartupBootstrapRuntime keeps log-level precedence for repeated calls", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({
|
||||
logLevel: "warn",
|
||||
});
|
||||
|
||||
runStartupBootstrapRuntimeService({
|
||||
runStartupBootstrapRuntime({
|
||||
argv: ["node", "main.ts", "--log-level", "warn"],
|
||||
parseArgs: () => args,
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
@@ -102,13 +102,13 @@ test("runStartupBootstrapRuntimeService keeps log-level precedence for repeated
|
||||
]);
|
||||
});
|
||||
|
||||
test("runStartupBootstrapRuntimeService keeps --debug separate from log verbosity", () => {
|
||||
test("runStartupBootstrapRuntime keeps --debug separate from log verbosity", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({
|
||||
debug: true,
|
||||
});
|
||||
|
||||
runStartupBootstrapRuntimeService({
|
||||
runStartupBootstrapRuntime({
|
||||
argv: ["node", "main.ts", "--debug"],
|
||||
parseArgs: () => args,
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
@@ -123,11 +123,11 @@ test("runStartupBootstrapRuntimeService keeps --debug separate from log verbosit
|
||||
assert.deepEqual(calls, ["forceX11", "enforceWayland", "startLifecycle"]);
|
||||
});
|
||||
|
||||
test("runStartupBootstrapRuntimeService skips lifecycle when generate-config flow handled", () => {
|
||||
test("runStartupBootstrapRuntime skips lifecycle when generate-config flow handled", () => {
|
||||
const calls: string[] = [];
|
||||
const args = makeArgs({ generateConfig: true, logLevel: "warn" });
|
||||
|
||||
const result = runStartupBootstrapRuntimeService({
|
||||
const result = runStartupBootstrapRuntime({
|
||||
argv: ["node", "main.ts", "--generate-config"],
|
||||
parseArgs: () => args,
|
||||
setLogLevel: (level, source) => calls.push(`setLog:${level}:${source}`),
|
||||
@@ -40,7 +40,7 @@ export interface StartupBootstrapRuntimeDeps {
|
||||
startAppLifecycle: (args: CliArgs) => void;
|
||||
}
|
||||
|
||||
export function runStartupBootstrapRuntimeService(
|
||||
export function runStartupBootstrapRuntime(
|
||||
deps: StartupBootstrapRuntimeDeps,
|
||||
): StartupBootstrapRuntimeState {
|
||||
const initialArgs = deps.parseArgs(deps.argv);
|
||||
@@ -107,7 +107,7 @@ export interface AppReadyRuntimeDeps {
|
||||
handleInitialArgs: () => void;
|
||||
}
|
||||
|
||||
export function getInitialInvisibleOverlayVisibilityService(
|
||||
export function getInitialInvisibleOverlayVisibility(
|
||||
config: RuntimeConfigLike,
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
@@ -118,7 +118,7 @@ export function getInitialInvisibleOverlayVisibilityService(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldAutoInitializeOverlayRuntimeFromConfigService(
|
||||
export function shouldAutoInitializeOverlayRuntimeFromConfig(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
if (config.auto_start_overlay === true) return true;
|
||||
@@ -126,13 +126,13 @@ export function shouldAutoInitializeOverlayRuntimeFromConfigService(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldBindVisibleOverlayToMpvSubVisibilityService(
|
||||
export function shouldBindVisibleOverlayToMpvSubVisibility(
|
||||
config: RuntimeConfigLike,
|
||||
): boolean {
|
||||
return config.bind_visible_overlay_to_mpv_sub_visibility;
|
||||
}
|
||||
|
||||
export function isAutoUpdateEnabledRuntimeService(
|
||||
export function isAutoUpdateEnabledRuntime(
|
||||
config: ResolvedConfig | RuntimeConfigLike,
|
||||
runtimeOptionsManager: RuntimeAutoUpdateOptionManagerLike | null,
|
||||
): boolean {
|
||||
@@ -141,7 +141,7 @@ export function isAutoUpdateEnabledRuntimeService(
|
||||
return (config as ResolvedConfig).ankiConnect?.behavior?.autoUpdateNewCards !== false;
|
||||
}
|
||||
|
||||
export async function runAppReadyRuntimeService(
|
||||
export async function runAppReadyRuntime(
|
||||
deps: AppReadyRuntimeDeps,
|
||||
): Promise<void> {
|
||||
deps.loadSubtitlePosition();
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
SubsyncResult,
|
||||
} from "../../types";
|
||||
import { SubsyncResolvedConfig } from "../../subsync/utils";
|
||||
import { runSubsyncManualFromIpcService } from "./ipc-command-service";
|
||||
import { runSubsyncManualFromIpc } from "./ipc-command";
|
||||
import {
|
||||
TriggerSubsyncFromConfigDeps,
|
||||
runSubsyncManualService,
|
||||
triggerSubsyncFromConfigService,
|
||||
} from "./subsync-service";
|
||||
runSubsyncManual,
|
||||
triggerSubsyncFromConfig,
|
||||
} from "./subsync";
|
||||
|
||||
const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
||||
|
||||
@@ -62,24 +62,24 @@ function buildTriggerSubsyncDeps(
|
||||
};
|
||||
}
|
||||
|
||||
export async function triggerSubsyncFromConfigRuntimeService(
|
||||
export async function triggerSubsyncFromConfigRuntime(
|
||||
deps: SubsyncRuntimeDeps,
|
||||
): Promise<void> {
|
||||
await triggerSubsyncFromConfigService(buildTriggerSubsyncDeps(deps));
|
||||
await triggerSubsyncFromConfig(buildTriggerSubsyncDeps(deps));
|
||||
}
|
||||
|
||||
export async function runSubsyncManualFromIpcRuntimeService(
|
||||
export async function runSubsyncManualFromIpcRuntime(
|
||||
request: SubsyncManualRunRequest,
|
||||
deps: SubsyncRuntimeDeps,
|
||||
): Promise<SubsyncResult> {
|
||||
const triggerDeps = buildTriggerSubsyncDeps(deps);
|
||||
return runSubsyncManualFromIpcService(request, {
|
||||
return runSubsyncManualFromIpc(request, {
|
||||
isSubsyncInProgress: triggerDeps.isSubsyncInProgress,
|
||||
setSubsyncInProgress: triggerDeps.setSubsyncInProgress,
|
||||
showMpvOsd: triggerDeps.showMpvOsd,
|
||||
runWithSpinner: (task) =>
|
||||
triggerDeps.runWithSubsyncSpinner(() => task()),
|
||||
runSubsyncManual: (subsyncRequest) =>
|
||||
runSubsyncManualService(subsyncRequest, triggerDeps),
|
||||
runSubsyncManual(subsyncRequest, triggerDeps),
|
||||
});
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import * as os from "os";
|
||||
import * as path from "path";
|
||||
import {
|
||||
TriggerSubsyncFromConfigDeps,
|
||||
runSubsyncManualService,
|
||||
triggerSubsyncFromConfigService,
|
||||
} from "./subsync-service";
|
||||
runSubsyncManual,
|
||||
triggerSubsyncFromConfig,
|
||||
} from "./subsync";
|
||||
|
||||
function makeDeps(
|
||||
overrides: Partial<TriggerSubsyncFromConfigDeps> = {},
|
||||
@@ -55,9 +55,9 @@ function makeDeps(
|
||||
};
|
||||
}
|
||||
|
||||
test("triggerSubsyncFromConfigService returns early when already in progress", async () => {
|
||||
test("triggerSubsyncFromConfig returns early when already in progress", async () => {
|
||||
const osd: string[] = [];
|
||||
await triggerSubsyncFromConfigService(
|
||||
await triggerSubsyncFromConfig(
|
||||
makeDeps({
|
||||
isSubsyncInProgress: () => true,
|
||||
showMpvOsd: (text) => {
|
||||
@@ -68,12 +68,12 @@ test("triggerSubsyncFromConfigService returns early when already in progress", a
|
||||
assert.deepEqual(osd, ["Subsync already running"]);
|
||||
});
|
||||
|
||||
test("triggerSubsyncFromConfigService opens manual picker in manual mode", async () => {
|
||||
test("triggerSubsyncFromConfig opens manual picker in manual mode", async () => {
|
||||
const osd: string[] = [];
|
||||
let payloadTrackCount = 0;
|
||||
let inProgressState: boolean | null = null;
|
||||
|
||||
await triggerSubsyncFromConfigService(
|
||||
await triggerSubsyncFromConfig(
|
||||
makeDeps({
|
||||
openManualPicker: (payload) => {
|
||||
payloadTrackCount = payload.sourceTracks.length;
|
||||
@@ -92,9 +92,9 @@ test("triggerSubsyncFromConfigService opens manual picker in manual mode", async
|
||||
assert.equal(inProgressState, false);
|
||||
});
|
||||
|
||||
test("triggerSubsyncFromConfigService reports failures to OSD", async () => {
|
||||
test("triggerSubsyncFromConfig reports failures to OSD", async () => {
|
||||
const osd: string[] = [];
|
||||
await triggerSubsyncFromConfigService(
|
||||
await triggerSubsyncFromConfig(
|
||||
makeDeps({
|
||||
getMpvClient: () => null,
|
||||
showMpvOsd: (text) => {
|
||||
@@ -106,8 +106,8 @@ test("triggerSubsyncFromConfigService reports failures to OSD", async () => {
|
||||
assert.ok(osd.some((line) => line.startsWith("Subsync failed: MPV not connected")));
|
||||
});
|
||||
|
||||
test("runSubsyncManualService requires a source track for alass", async () => {
|
||||
const result = await runSubsyncManualService(
|
||||
test("runSubsyncManual requires a source track for alass", async () => {
|
||||
const result = await runSubsyncManual(
|
||||
{ engine: "alass", sourceTrackId: null },
|
||||
makeDeps(),
|
||||
);
|
||||
@@ -118,11 +118,11 @@ test("runSubsyncManualService requires a source track for alass", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("triggerSubsyncFromConfigService reports path validation failures", async () => {
|
||||
test("triggerSubsyncFromConfig reports path validation failures", async () => {
|
||||
const osd: string[] = [];
|
||||
const inProgress: boolean[] = [];
|
||||
|
||||
await triggerSubsyncFromConfigService(
|
||||
await triggerSubsyncFromConfig(
|
||||
makeDeps({
|
||||
getResolvedConfig: () => ({
|
||||
defaultMode: "auto",
|
||||
@@ -152,7 +152,7 @@ function writeExecutableScript(filePath: string, content: string): void {
|
||||
fs.chmodSync(filePath, 0o755);
|
||||
}
|
||||
|
||||
test("runSubsyncManualService constructs ffsubsync command and returns success", async () => {
|
||||
test("runSubsyncManual constructs ffsubsync command and returns success", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-ffsubsync-"));
|
||||
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log");
|
||||
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
|
||||
@@ -210,7 +210,7 @@ test("runSubsyncManualService constructs ffsubsync command and returns success",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await runSubsyncManualService(
|
||||
const result = await runSubsyncManual(
|
||||
{ engine: "ffsubsync", sourceTrackId: null },
|
||||
deps,
|
||||
);
|
||||
@@ -227,7 +227,7 @@ test("runSubsyncManualService constructs ffsubsync command and returns success",
|
||||
assert.deepEqual(sentCommands[1], ["set_property", "sub-delay", 0]);
|
||||
});
|
||||
|
||||
test("runSubsyncManualService constructs alass command and returns failure on non-zero exit", async () => {
|
||||
test("runSubsyncManual constructs alass command and returns failure on non-zero exit", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-alass-"));
|
||||
const alassLogPath = path.join(tmpDir, "alass-args.log");
|
||||
const alassPath = path.join(tmpDir, "alass.sh");
|
||||
@@ -285,7 +285,7 @@ test("runSubsyncManualService constructs alass command and returns failure on no
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await runSubsyncManualService(
|
||||
const result = await runSubsyncManual(
|
||||
{ engine: "alass", sourceTrackId: 2 },
|
||||
deps,
|
||||
);
|
||||
@@ -298,7 +298,7 @@ test("runSubsyncManualService constructs alass command and returns failure on no
|
||||
assert.equal(alassArgs[1], primaryPath);
|
||||
});
|
||||
|
||||
test("runSubsyncManualService resolves string sid values from mpv stream properties", async () => {
|
||||
test("runSubsyncManual resolves string sid values from mpv stream properties", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "subsync-stream-sid-"));
|
||||
const ffsubsyncPath = path.join(tmpDir, "ffsubsync.sh");
|
||||
const ffsubsyncLogPath = path.join(tmpDir, "ffsubsync-args.log");
|
||||
@@ -347,7 +347,7 @@ test("runSubsyncManualService resolves string sid values from mpv stream propert
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await runSubsyncManualService(
|
||||
const result = await runSubsyncManual(
|
||||
{ engine: "ffsubsync", sourceTrackId: null },
|
||||
deps,
|
||||
);
|
||||
@@ -399,7 +399,7 @@ async function runSubsyncAutoInternal(
|
||||
);
|
||||
}
|
||||
|
||||
export async function runSubsyncManualService(
|
||||
export async function runSubsyncManual(
|
||||
request: SubsyncManualRunRequest,
|
||||
deps: SubsyncCoreDeps,
|
||||
): Promise<SubsyncResult> {
|
||||
@@ -452,7 +452,7 @@ export async function runSubsyncManualService(
|
||||
}
|
||||
}
|
||||
|
||||
export async function openSubsyncManualPickerService(
|
||||
export async function openSubsyncManualPicker(
|
||||
deps: TriggerSubsyncFromConfigDeps,
|
||||
): Promise<void> {
|
||||
const client = getMpvClientForSubsync(deps);
|
||||
@@ -468,7 +468,7 @@ export async function openSubsyncManualPickerService(
|
||||
deps.openManualPicker(payload);
|
||||
}
|
||||
|
||||
export async function triggerSubsyncFromConfigService(
|
||||
export async function triggerSubsyncFromConfig(
|
||||
deps: TriggerSubsyncFromConfigDeps,
|
||||
): Promise<void> {
|
||||
if (deps.isSubsyncInProgress()) {
|
||||
@@ -479,7 +479,7 @@ export async function triggerSubsyncFromConfigService(
|
||||
const resolved = deps.getResolvedConfig();
|
||||
try {
|
||||
if (resolved.defaultMode === "manual") {
|
||||
await openSubsyncManualPickerService(deps);
|
||||
await openSubsyncManualPicker(deps);
|
||||
deps.showMpvOsd("Subsync: choose engine and source");
|
||||
return;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export interface CycleSecondarySubModeDeps {
|
||||
const SECONDARY_SUB_CYCLE: SecondarySubMode[] = ["hidden", "visible", "hover"];
|
||||
const SECONDARY_SUB_TOGGLE_DEBOUNCE_MS = 120;
|
||||
|
||||
export function cycleSecondarySubModeService(
|
||||
export function cycleSecondarySubMode(
|
||||
deps: CycleSecondarySubModeDeps,
|
||||
): void {
|
||||
const now = deps.now ? deps.now() : Date.now();
|
||||
@@ -89,7 +89,7 @@ function persistSubtitlePosition(
|
||||
fs.writeFileSync(positionPath, JSON.stringify(position, null, 2));
|
||||
}
|
||||
|
||||
export function loadSubtitlePositionService(options: {
|
||||
export function loadSubtitlePosition(options: {
|
||||
currentMediaPath: string | null;
|
||||
fallbackPosition: SubtitlePosition;
|
||||
} & { subtitlePositionsDir: string }): SubtitlePosition | null {
|
||||
@@ -135,7 +135,7 @@ export function loadSubtitlePositionService(options: {
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSubtitlePositionService(options: {
|
||||
export function saveSubtitlePosition(options: {
|
||||
position: SubtitlePosition;
|
||||
currentMediaPath: string | null;
|
||||
subtitlePositionsDir: string;
|
||||
@@ -160,7 +160,7 @@ export function saveSubtitlePositionService(options: {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateCurrentMediaPathService(options: {
|
||||
export function updateCurrentMediaPath(options: {
|
||||
mediaPath: unknown;
|
||||
currentMediaPath: string | null;
|
||||
pendingSubtitlePosition: SubtitlePosition | null;
|
||||
@@ -16,7 +16,7 @@ export function hasMpvWebsocketPlugin(): boolean {
|
||||
return fs.existsSync(mpvWebsocketPath);
|
||||
}
|
||||
|
||||
export class SubtitleWebSocketService {
|
||||
export class SubtitleWebSocket {
|
||||
private server: WebSocket.Server | null = null;
|
||||
|
||||
public isRunning(): boolean {
|
||||
@@ -5,7 +5,7 @@ import { createLogger } from "../../logger";
|
||||
|
||||
const logger = createLogger("main:texthooker");
|
||||
|
||||
export class TexthookerService {
|
||||
export class Texthooker {
|
||||
private server: http.Server | null = null;
|
||||
|
||||
public isRunning(): boolean {
|
||||
@@ -2,11 +2,11 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PartOfSpeech } from "../../types";
|
||||
import {
|
||||
createTokenizerDepsRuntimeService,
|
||||
createTokenizerDepsRuntime,
|
||||
TokenizerServiceDeps,
|
||||
TokenizerDepsRuntimeOptions,
|
||||
tokenizeSubtitleService,
|
||||
} from "./tokenizer-service";
|
||||
tokenizeSubtitle,
|
||||
} from "./tokenizer";
|
||||
|
||||
function makeDeps(
|
||||
overrides: Partial<TokenizerServiceDeps> = {},
|
||||
@@ -31,7 +31,7 @@ function makeDepsFromMecabTokenizer(
|
||||
tokenize: (text: string) => Promise<import("../../types").Token[] | null>,
|
||||
overrides: Partial<TokenizerDepsRuntimeOptions> = {},
|
||||
): TokenizerServiceDeps {
|
||||
return createTokenizerDepsRuntimeService({
|
||||
return createTokenizerDepsRuntime({
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => {},
|
||||
@@ -49,8 +49,8 @@ function makeDepsFromMecabTokenizer(
|
||||
});
|
||||
}
|
||||
|
||||
test("tokenizeSubtitleService assigns JLPT level to parsed Yomitan tokens", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle assigns JLPT level to parsed Yomitan tokens", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -88,9 +88,9 @@ test("tokenizeSubtitleService assigns JLPT level to parsed Yomitan tokens", asyn
|
||||
assert.equal(result.tokens?.[0]?.jlptLevel, "N5");
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService caches JLPT lookups across repeated tokens", async () => {
|
||||
test("tokenizeSubtitle caches JLPT lookups across repeated tokens", async () => {
|
||||
let lookupCalls = 0;
|
||||
const result = await tokenizeSubtitleService(
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫猫",
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
@@ -133,8 +133,8 @@ test("tokenizeSubtitleService caches JLPT lookups across repeated tokens", async
|
||||
assert.equal(result.tokens?.[1]?.jlptLevel, "N5");
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService leaves JLPT unset for non-matching tokens", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle leaves JLPT unset for non-matching tokens", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫",
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
@@ -159,9 +159,9 @@ test("tokenizeSubtitleService leaves JLPT unset for non-matching tokens", async
|
||||
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService skips JLPT lookups when disabled", async () => {
|
||||
test("tokenizeSubtitle skips JLPT lookups when disabled", async () => {
|
||||
let lookupCalls = 0;
|
||||
const result = await tokenizeSubtitleService(
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async () => [
|
||||
@@ -190,8 +190,8 @@ test("tokenizeSubtitleService skips JLPT lookups when disabled", async () => {
|
||||
assert.equal(lookupCalls, 0);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService applies frequency dictionary ranks", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle applies frequency dictionary ranks", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -228,8 +228,8 @@ test("tokenizeSubtitleService applies frequency dictionary ranks", async () => {
|
||||
assert.equal(result.tokens?.[1]?.frequencyRank, 1200);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService uses only selected Yomitan headword for frequency lookup", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle uses only selected Yomitan headword for frequency lookup", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -265,8 +265,8 @@ test("tokenizeSubtitleService uses only selected Yomitan headword for frequency
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, 1200);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService keeps furigana-split Yomitan segments as one token", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle keeps furigana-split Yomitan segments as one token", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"友達と話した",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -324,8 +324,8 @@ test("tokenizeSubtitleService keeps furigana-split Yomitan segments as one token
|
||||
assert.equal(result.tokens?.[2]?.frequencyRank, 90);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService prefers exact headword frequency over surface/reading when available", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle prefers exact headword frequency over surface/reading when available", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -358,8 +358,8 @@ test("tokenizeSubtitleService prefers exact headword frequency over surface/read
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, 8);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService keeps no frequency when only reading matches and headword misses", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle keeps no frequency when only reading matches and headword misses", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -392,8 +392,8 @@ test("tokenizeSubtitleService keeps no frequency when only reading matches and h
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService ignores invalid frequency rank on selected headword", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle ignores invalid frequency rank on selected headword", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -429,8 +429,8 @@ test("tokenizeSubtitleService ignores invalid frequency rank on selected headwor
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService handles real-word frequency candidates and prefers most frequent term", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle handles real-word frequency candidates and prefers most frequent term", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"昨日",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -466,8 +466,8 @@ test("tokenizeSubtitleService handles real-word frequency candidates and prefers
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, 40);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService ignores candidates with no dictionary rank when higher-frequency candidate exists", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle ignores candidates with no dictionary rank when higher-frequency candidate exists", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -504,8 +504,8 @@ test("tokenizeSubtitleService ignores candidates with no dictionary rank when hi
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, 88);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService ignores frequency lookup failures", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle ignores frequency lookup failures", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -531,8 +531,8 @@ test("tokenizeSubtitleService ignores frequency lookup failures", async () => {
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService skips frequency rank when Yomitan token is enriched as particle by mecab pos1", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle skips frequency rank when Yomitan token is enriched as particle by mecab pos1", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"は",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -580,8 +580,8 @@ test("tokenizeSubtitleService skips frequency rank when Yomitan token is enriche
|
||||
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService ignores invalid frequency ranks", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle ignores invalid frequency ranks", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
@@ -622,9 +622,9 @@ test("tokenizeSubtitleService ignores invalid frequency ranks", async () => {
|
||||
assert.equal(result.tokens?.[1]?.frequencyRank, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService skips frequency lookups when disabled", async () => {
|
||||
test("tokenizeSubtitle skips frequency lookups when disabled", async () => {
|
||||
let frequencyCalls = 0;
|
||||
const result = await tokenizeSubtitleService(
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫",
|
||||
makeDeps({
|
||||
getFrequencyDictionaryEnabled: () => false,
|
||||
@@ -653,8 +653,8 @@ test("tokenizeSubtitleService skips frequency lookups when disabled", async () =
|
||||
assert.equal(frequencyCalls, 0);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService skips JLPT level for excluded demonstratives", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle skips JLPT level for excluded demonstratives", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"この",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -687,8 +687,8 @@ test("tokenizeSubtitleService skips JLPT level for excluded demonstratives", asy
|
||||
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService skips JLPT level for repeated kana SFX", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle skips JLPT level for repeated kana SFX", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"ああ",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -721,8 +721,8 @@ test("tokenizeSubtitleService skips JLPT level for repeated kana SFX", async ()
|
||||
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService assigns JLPT level to mecab tokens", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle assigns JLPT level to mecab tokens", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
@@ -747,8 +747,8 @@ test("tokenizeSubtitleService assigns JLPT level to mecab tokens", async () => {
|
||||
assert.equal(result.tokens?.[0]?.jlptLevel, "N4");
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService skips JLPT level for mecab tokens marked as ineligible", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle skips JLPT level for mecab tokens marked as ineligible", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"は",
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
@@ -774,14 +774,14 @@ test("tokenizeSubtitleService skips JLPT level for mecab tokens marked as inelig
|
||||
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService returns null tokens for empty normalized text", async () => {
|
||||
const result = await tokenizeSubtitleService(" \\n ", makeDeps());
|
||||
test("tokenizeSubtitle returns null tokens for empty normalized text", async () => {
|
||||
const result = await tokenizeSubtitle(" \\n ", makeDeps());
|
||||
assert.deepEqual(result, { text: " \\n ", tokens: null });
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService normalizes newlines before mecab fallback", async () => {
|
||||
test("tokenizeSubtitle normalizes newlines before mecab fallback", async () => {
|
||||
let tokenizeInput = "";
|
||||
const result = await tokenizeSubtitleService(
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫\\Nです\nね",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async (text) => {
|
||||
@@ -808,8 +808,8 @@ test("tokenizeSubtitleService normalizes newlines before mecab fallback", async
|
||||
assert.equal(result.tokens?.[0]?.surface, "猫ですね");
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService falls back to mecab tokens when available", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle falls back to mecab tokens when available", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async () => [
|
||||
@@ -833,8 +833,8 @@ test("tokenizeSubtitleService falls back to mecab tokens when available", async
|
||||
assert.equal(result.tokens?.[0]?.surface, "猫");
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService returns null tokens when mecab throws", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle returns null tokens when mecab throws", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async () => {
|
||||
@@ -846,7 +846,7 @@ test("tokenizeSubtitleService returns null tokens when mecab throws", async () =
|
||||
assert.deepEqual(result, { text: "猫です", tokens: null });
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService uses Yomitan parser result when available", async () => {
|
||||
test("tokenizeSubtitle uses Yomitan parser result when available", async () => {
|
||||
const parserWindow = {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
@@ -874,7 +874,7 @@ test("tokenizeSubtitleService uses Yomitan parser result when available", async
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
|
||||
const result = await tokenizeSubtitleService(
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -893,7 +893,7 @@ test("tokenizeSubtitleService uses Yomitan parser result when available", async
|
||||
assert.equal(result.tokens?.[1]?.isKnown, false);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService logs selected Yomitan groups when debug toggle is enabled", async () => {
|
||||
test("tokenizeSubtitle logs selected Yomitan groups when debug toggle is enabled", async () => {
|
||||
const infoLogs: string[] = [];
|
||||
const originalInfo = console.info;
|
||||
console.info = (...args: unknown[]) => {
|
||||
@@ -901,7 +901,7 @@ test("tokenizeSubtitleService logs selected Yomitan groups when debug toggle is
|
||||
};
|
||||
|
||||
try {
|
||||
await tokenizeSubtitleService(
|
||||
await tokenizeSubtitle(
|
||||
"友達と話した",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -949,7 +949,7 @@ test("tokenizeSubtitleService logs selected Yomitan groups when debug toggle is
|
||||
);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService does not log Yomitan groups when debug toggle is disabled", async () => {
|
||||
test("tokenizeSubtitle does not log Yomitan groups when debug toggle is disabled", async () => {
|
||||
const infoLogs: string[] = [];
|
||||
const originalInfo = console.info;
|
||||
console.info = (...args: unknown[]) => {
|
||||
@@ -957,7 +957,7 @@ test("tokenizeSubtitleService does not log Yomitan groups when debug toggle is d
|
||||
};
|
||||
|
||||
try {
|
||||
await tokenizeSubtitleService(
|
||||
await tokenizeSubtitle(
|
||||
"友達と話した",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -999,7 +999,7 @@ test("tokenizeSubtitleService does not log Yomitan groups when debug toggle is d
|
||||
);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService preserves segmented Yomitan line as one token", async () => {
|
||||
test("tokenizeSubtitle preserves segmented Yomitan line as one token", async () => {
|
||||
const parserWindow = {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
@@ -1025,7 +1025,7 @@ test("tokenizeSubtitleService preserves segmented Yomitan line as one token", as
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
|
||||
const result = await tokenizeSubtitleService(
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -1042,8 +1042,8 @@ test("tokenizeSubtitleService preserves segmented Yomitan line as one token", as
|
||||
assert.equal(result.tokens?.[0]?.isKnown, false);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService prefers mecab parser tokens when scanning parser returns one token", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle prefers mecab parser tokens when scanning parser returns one token", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"俺は小園にいきたい",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -1091,8 +1091,8 @@ test("tokenizeSubtitleService prefers mecab parser tokens when scanning parser r
|
||||
assert.equal(result.tokens?.[2]?.frequencyRank, 25);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService keeps scanning parser tokens when they are already split", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle keeps scanning parser tokens when they are already split", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"小園に行きたい",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -1139,8 +1139,8 @@ test("tokenizeSubtitleService keeps scanning parser tokens when they are already
|
||||
assert.equal(result.tokens?.[2]?.frequencyRank, undefined);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService prefers parse candidates with fewer fragment-only kana tokens when source priority is equal", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle prefers parse candidates with fewer fragment-only kana tokens when source priority is equal", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"俺は公園にいきたい",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -1192,8 +1192,8 @@ test("tokenizeSubtitleService prefers parse candidates with fewer fragment-only
|
||||
assert.equal(result.tokens?.[4]?.frequencyRank, 1500);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService still assigns frequency to non-known Yomitan tokens", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle still assigns frequency to non-known Yomitan tokens", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"小園に",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -1229,8 +1229,8 @@ test("tokenizeSubtitleService still assigns frequency to non-known Yomitan token
|
||||
assert.equal(result.tokens?.[1]?.frequencyRank, 3000);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService marks tokens as known using callback", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle marks tokens as known using callback", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
@@ -1255,8 +1255,8 @@ test("tokenizeSubtitleService marks tokens as known using callback", async () =>
|
||||
assert.equal(result.tokens?.[0]?.isKnown, true);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService still assigns frequency rank to non-known tokens", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle still assigns frequency rank to non-known tokens", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"既知未知",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async () => [
|
||||
@@ -1312,8 +1312,8 @@ test("tokenizeSubtitleService still assigns frequency rank to non-known tokens",
|
||||
assert.equal(result.tokens?.[1]?.frequencyRank, 30);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService selects one N+1 target token", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle selects one N+1 target token", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async () => [
|
||||
@@ -1349,8 +1349,8 @@ test("tokenizeSubtitleService selects one N+1 target token", async () => {
|
||||
assert.equal(targets[0]?.surface, "犬");
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService does not mark target when sentence has multiple candidates", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle does not mark target when sentence has multiple candidates", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫犬",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async () => [
|
||||
@@ -1386,7 +1386,7 @@ test("tokenizeSubtitleService does not mark target when sentence has multiple ca
|
||||
);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService applies N+1 target marking to Yomitan results", async () => {
|
||||
test("tokenizeSubtitle applies N+1 target marking to Yomitan results", async () => {
|
||||
const parserWindow = {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
@@ -1415,7 +1415,7 @@ test("tokenizeSubtitleService applies N+1 target marking to Yomitan results", as
|
||||
},
|
||||
} as unknown as Electron.BrowserWindow;
|
||||
|
||||
const result = await tokenizeSubtitleService(
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
getYomitanExt: () => ({ id: "dummy-ext" } as any),
|
||||
@@ -1433,8 +1433,8 @@ test("tokenizeSubtitleService applies N+1 target marking to Yomitan results", as
|
||||
assert.equal(result.tokens?.[1]?.isNPlusOneTarget, false);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService does not color 1-2 word sentences by default", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle does not color 1-2 word sentences by default", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDeps({
|
||||
tokenizeWithMecab: async () => [
|
||||
@@ -1470,8 +1470,8 @@ test("tokenizeSubtitleService does not color 1-2 word sentences by default", asy
|
||||
);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService checks known words by headword, not surface", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle checks known words by headword, not surface", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
@@ -1496,8 +1496,8 @@ test("tokenizeSubtitleService checks known words by headword, not surface", asyn
|
||||
assert.equal(result.tokens?.[0]?.isKnown, true);
|
||||
});
|
||||
|
||||
test("tokenizeSubtitleService checks known words by surface when configured", async () => {
|
||||
const result = await tokenizeSubtitleService(
|
||||
test("tokenizeSubtitle checks known words by surface when configured", async () => {
|
||||
const result = await tokenizeSubtitle(
|
||||
"猫です",
|
||||
makeDepsFromMecabTokenizer(async () => [
|
||||
{
|
||||
@@ -182,7 +182,7 @@ function getCachedFrequencyRank(
|
||||
return rank;
|
||||
}
|
||||
|
||||
export function createTokenizerDepsRuntimeService(
|
||||
export function createTokenizerDepsRuntime(
|
||||
options: TokenizerDepsRuntimeOptions,
|
||||
): TokenizerServiceDeps {
|
||||
return {
|
||||
@@ -983,7 +983,7 @@ async function parseWithYomitanInternalParser(
|
||||
}
|
||||
}
|
||||
|
||||
export async function tokenizeSubtitleService(
|
||||
export async function tokenizeSubtitle(
|
||||
text: string,
|
||||
deps: TokenizerServiceDeps,
|
||||
): Promise<SubtitleData> {
|
||||
@@ -58,7 +58,7 @@ function ensureExtensionCopy(sourceDir: string, userDataPath: string): string {
|
||||
return targetDir;
|
||||
}
|
||||
|
||||
export async function loadYomitanExtensionService(
|
||||
export async function loadYomitanExtension(
|
||||
deps: YomitanExtensionLoaderDeps,
|
||||
): Promise<Extension | null> {
|
||||
const searchPaths = [
|
||||
Reference in New Issue
Block a user