mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor runtime deps wiring and docs/config updates
This commit is contained in:
@@ -55,7 +55,7 @@ export function generateConfigTemplate(config: ResolvedConfig = deepCloneConfig(
|
||||
lines.push(" * SubMiner Example Configuration File");
|
||||
lines.push(" *");
|
||||
lines.push(" * This file is auto-generated from src/config/definitions.ts.");
|
||||
lines.push(" * Copy to ~/.config/SubMiner/config.jsonc and edit as needed.");
|
||||
lines.push(" * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.");
|
||||
lines.push(" */");
|
||||
lines.push("{");
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
AnkiJimakuIpcDepsRuntimeOptions,
|
||||
createAnkiJimakuIpcDepsRuntimeService,
|
||||
} from "./anki-jimaku-ipc-deps-runtime-service";
|
||||
|
||||
test("createAnkiJimakuIpcDepsRuntimeService returns passthrough runtime options", async () => {
|
||||
const calls: string[] = [];
|
||||
const options = {
|
||||
patchAnkiConnectEnabled: () => calls.push("patch"),
|
||||
getResolvedConfig: () => ({ ankiConnect: undefined }),
|
||||
getRuntimeOptionsManager: () => null,
|
||||
getSubtitleTimingTracker: () => null,
|
||||
getMpvClient: () => null,
|
||||
getAnkiIntegration: () => null,
|
||||
setAnkiIntegration: () => calls.push("set-integration"),
|
||||
showDesktopNotification: () => calls.push("notify"),
|
||||
createFieldGroupingCallback: () => async () => ({
|
||||
keepNoteId: 0,
|
||||
deleteNoteId: 0,
|
||||
deleteDuplicate: false,
|
||||
cancelled: true,
|
||||
}),
|
||||
broadcastRuntimeOptionsChanged: () => calls.push("broadcast"),
|
||||
getFieldGroupingResolver: () => null,
|
||||
setFieldGroupingResolver: () => calls.push("set-resolver"),
|
||||
parseMediaInfo: () => ({ mediaPath: null, baseName: null, episode: null }),
|
||||
getCurrentMediaPath: () => "/tmp/a.mp4",
|
||||
jimakuFetchJson: async () => ({ ok: true, data: [] }),
|
||||
getJimakuMaxEntryResults: () => 100,
|
||||
getJimakuLanguagePreference: () => "prefer-japanese",
|
||||
resolveJimakuApiKey: async () => "abc",
|
||||
isRemoteMediaPath: () => false,
|
||||
downloadToFile: async () => ({ ok: true, path: "/tmp/a.srt" }),
|
||||
} as unknown as AnkiJimakuIpcDepsRuntimeOptions;
|
||||
|
||||
const runtime = createAnkiJimakuIpcDepsRuntimeService(options);
|
||||
|
||||
runtime.patchAnkiConnectEnabled(true);
|
||||
runtime.broadcastRuntimeOptionsChanged();
|
||||
runtime.setFieldGroupingResolver(null);
|
||||
|
||||
assert.deepEqual(calls, ["patch", "broadcast", "set-resolver"]);
|
||||
assert.equal(runtime.getCurrentMediaPath(), "/tmp/a.mp4");
|
||||
assert.equal(runtime.getJimakuMaxEntryResults(), 100);
|
||||
assert.equal(await runtime.resolveJimakuApiKey(), "abc");
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
AnkiJimakuIpcRuntimeOptions,
|
||||
} from "./anki-jimaku-runtime-service";
|
||||
|
||||
export type AnkiJimakuIpcDepsRuntimeOptions = AnkiJimakuIpcRuntimeOptions;
|
||||
|
||||
export function createAnkiJimakuIpcDepsRuntimeService(
|
||||
options: AnkiJimakuIpcDepsRuntimeOptions,
|
||||
): AnkiJimakuIpcRuntimeOptions {
|
||||
return {
|
||||
patchAnkiConnectEnabled: options.patchAnkiConnectEnabled,
|
||||
getResolvedConfig: options.getResolvedConfig,
|
||||
getRuntimeOptionsManager: options.getRuntimeOptionsManager,
|
||||
getSubtitleTimingTracker: options.getSubtitleTimingTracker,
|
||||
getMpvClient: options.getMpvClient,
|
||||
getAnkiIntegration: options.getAnkiIntegration,
|
||||
setAnkiIntegration: options.setAnkiIntegration,
|
||||
showDesktopNotification: options.showDesktopNotification,
|
||||
createFieldGroupingCallback: options.createFieldGroupingCallback,
|
||||
broadcastRuntimeOptionsChanged: options.broadcastRuntimeOptionsChanged,
|
||||
getFieldGroupingResolver: options.getFieldGroupingResolver,
|
||||
setFieldGroupingResolver: options.setFieldGroupingResolver,
|
||||
parseMediaInfo: options.parseMediaInfo,
|
||||
getCurrentMediaPath: options.getCurrentMediaPath,
|
||||
jimakuFetchJson: options.jimakuFetchJson,
|
||||
getJimakuMaxEntryResults: options.getJimakuMaxEntryResults,
|
||||
getJimakuLanguagePreference: options.getJimakuLanguagePreference,
|
||||
resolveJimakuApiKey: options.resolveJimakuApiKey,
|
||||
isRemoteMediaPath: options.isRemoteMediaPath,
|
||||
downloadToFile: options.downloadToFile,
|
||||
};
|
||||
}
|
||||
@@ -73,15 +73,12 @@ export {
|
||||
getOverlayWindowsRuntimeService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./overlay-broadcast-runtime-service";
|
||||
export { createMpvIpcClientDepsRuntimeService } from "./mpv-client-deps-runtime-service";
|
||||
export { createAppLifecycleDepsRuntimeService } from "./app-lifecycle-deps-runtime-service";
|
||||
export { createCliCommandDepsRuntimeService } from "./cli-command-deps-runtime-service";
|
||||
export { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service";
|
||||
export { createAnkiJimakuIpcDepsRuntimeService } from "./anki-jimaku-ipc-deps-runtime-service";
|
||||
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-runtime-service";
|
||||
export { createSubsyncRuntimeDepsService } from "./subsync-deps-runtime-service";
|
||||
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-runtime-service";
|
||||
export { createOverlayVisibilityFacadeDepsRuntimeService } from "./overlay-visibility-facade-deps-runtime-service";
|
||||
export { createMpvCommandIpcDepsRuntimeService } from "./mpv-command-ipc-deps-runtime-service";
|
||||
export { createRuntimeOptionsIpcDepsRuntimeService } from "./runtime-options-ipc-deps-runtime-service";
|
||||
export { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service";
|
||||
@@ -90,26 +87,8 @@ export {
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService,
|
||||
createOverlayWindowRuntimeDepsService,
|
||||
createVisibleOverlayVisibilityDepsRuntimeService,
|
||||
} from "./overlay-runtime-deps-service";
|
||||
export {
|
||||
createOverlayShortcutLifecycleDepsRuntimeService,
|
||||
createOverlayShortcutRuntimeDepsService,
|
||||
} from "./overlay-shortcut-runtime-deps-service";
|
||||
export {
|
||||
createCopyCurrentSubtitleDepsRuntimeService,
|
||||
createHandleMineSentenceDigitDepsRuntimeService,
|
||||
createHandleMultiCopyDigitDepsRuntimeService,
|
||||
createMarkLastCardAsAudioCardDepsRuntimeService,
|
||||
createMineSentenceCardDepsRuntimeService,
|
||||
createTriggerFieldGroupingDepsRuntimeService,
|
||||
createUpdateLastCardFromClipboardDepsRuntimeService,
|
||||
} from "./mining-runtime-deps-service";
|
||||
export {
|
||||
createGlobalShortcutRegistrationDepsRuntimeService,
|
||||
createSecondarySubtitleCycleDepsRuntimeService,
|
||||
createYomitanSettingsWindowDepsRuntimeService,
|
||||
runOverlayShortcutLocalFallbackRuntimeService,
|
||||
} from "./shortcut-ui-runtime-deps-service";
|
||||
} from "./overlay-deps-runtime-service";
|
||||
export { runOverlayShortcutLocalFallbackRuntimeService } from "./shortcut-ui-deps-runtime-service";
|
||||
export { createStartupLifecycleHooksRuntimeService } from "./startup-lifecycle-hooks-runtime-service";
|
||||
export { createRuntimeOptionsManagerRuntimeService } from "./runtime-options-manager-runtime-service";
|
||||
export { createAppLoggingRuntimeService } from "./app-logging-runtime-service";
|
||||
@@ -122,3 +101,4 @@ export { runStartupBootstrapRuntimeService } from "./startup-bootstrap-runtime-s
|
||||
export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runtime-service";
|
||||
export { updateInvisibleOverlayVisibilityService, updateVisibleOverlayVisibilityService } from "./overlay-visibility-service";
|
||||
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-runtime-service";
|
||||
export { createOverlayManagerService } from "./overlay-manager-service";
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createCopyCurrentSubtitleDepsRuntimeService,
|
||||
createHandleMineSentenceDigitDepsRuntimeService,
|
||||
createHandleMultiCopyDigitDepsRuntimeService,
|
||||
createMarkLastCardAsAudioCardDepsRuntimeService,
|
||||
createMineSentenceCardDepsRuntimeService,
|
||||
createTriggerFieldGroupingDepsRuntimeService,
|
||||
createUpdateLastCardFromClipboardDepsRuntimeService,
|
||||
} from "./mining-runtime-deps-service";
|
||||
|
||||
test("mining runtime deps builders preserve references", () => {
|
||||
const showMpvOsd = (_text: string) => {};
|
||||
const writeClipboardText = (_text: string) => {};
|
||||
const readClipboardText = () => "x";
|
||||
const logError = (_message: string, _err: unknown) => {};
|
||||
const subtitleTimingTracker = null;
|
||||
const ankiIntegration = null;
|
||||
const mpvClient = null;
|
||||
|
||||
const multiCopy = createHandleMultiCopyDigitDepsRuntimeService({
|
||||
subtitleTimingTracker,
|
||||
writeClipboardText,
|
||||
showMpvOsd,
|
||||
});
|
||||
const copyCurrent = createCopyCurrentSubtitleDepsRuntimeService({
|
||||
subtitleTimingTracker,
|
||||
writeClipboardText,
|
||||
showMpvOsd,
|
||||
});
|
||||
const updateLast = createUpdateLastCardFromClipboardDepsRuntimeService({
|
||||
ankiIntegration,
|
||||
readClipboardText,
|
||||
showMpvOsd,
|
||||
});
|
||||
const fieldGrouping = createTriggerFieldGroupingDepsRuntimeService({
|
||||
ankiIntegration,
|
||||
showMpvOsd,
|
||||
});
|
||||
const markAudio = createMarkLastCardAsAudioCardDepsRuntimeService({
|
||||
ankiIntegration,
|
||||
showMpvOsd,
|
||||
});
|
||||
const mineCard = createMineSentenceCardDepsRuntimeService({
|
||||
ankiIntegration,
|
||||
mpvClient,
|
||||
showMpvOsd,
|
||||
});
|
||||
const mineDigit = createHandleMineSentenceDigitDepsRuntimeService({
|
||||
subtitleTimingTracker,
|
||||
ankiIntegration,
|
||||
getCurrentSecondarySubText: () => undefined,
|
||||
showMpvOsd,
|
||||
logError,
|
||||
});
|
||||
|
||||
assert.equal(multiCopy.writeClipboardText, writeClipboardText);
|
||||
assert.equal(copyCurrent.showMpvOsd, showMpvOsd);
|
||||
assert.equal(updateLast.readClipboardText, readClipboardText);
|
||||
assert.equal(fieldGrouping.ankiIntegration, ankiIntegration);
|
||||
assert.equal(markAudio.showMpvOsd, showMpvOsd);
|
||||
assert.equal(mineCard.mpvClient, mpvClient);
|
||||
assert.equal(mineDigit.logError, logError);
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
import {
|
||||
copyCurrentSubtitleService,
|
||||
handleMineSentenceDigitService,
|
||||
handleMultiCopyDigitService,
|
||||
markLastCardAsAudioCardService,
|
||||
mineSentenceCardService,
|
||||
triggerFieldGroupingService,
|
||||
updateLastCardFromClipboardService,
|
||||
} from "./mining-runtime-service";
|
||||
|
||||
export function createHandleMultiCopyDigitDepsRuntimeService(
|
||||
options: {
|
||||
subtitleTimingTracker: Parameters<typeof handleMultiCopyDigitService>[1]["subtitleTimingTracker"];
|
||||
writeClipboardText: Parameters<typeof handleMultiCopyDigitService>[1]["writeClipboardText"];
|
||||
showMpvOsd: Parameters<typeof handleMultiCopyDigitService>[1]["showMpvOsd"];
|
||||
},
|
||||
): Parameters<typeof handleMultiCopyDigitService>[1] {
|
||||
return {
|
||||
subtitleTimingTracker: options.subtitleTimingTracker,
|
||||
writeClipboardText: options.writeClipboardText,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCopyCurrentSubtitleDepsRuntimeService(
|
||||
options: {
|
||||
subtitleTimingTracker: Parameters<typeof copyCurrentSubtitleService>[0]["subtitleTimingTracker"];
|
||||
writeClipboardText: Parameters<typeof copyCurrentSubtitleService>[0]["writeClipboardText"];
|
||||
showMpvOsd: Parameters<typeof copyCurrentSubtitleService>[0]["showMpvOsd"];
|
||||
},
|
||||
): Parameters<typeof copyCurrentSubtitleService>[0] {
|
||||
return {
|
||||
subtitleTimingTracker: options.subtitleTimingTracker,
|
||||
writeClipboardText: options.writeClipboardText,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function createUpdateLastCardFromClipboardDepsRuntimeService(
|
||||
options: {
|
||||
ankiIntegration: Parameters<typeof updateLastCardFromClipboardService>[0]["ankiIntegration"];
|
||||
readClipboardText: Parameters<typeof updateLastCardFromClipboardService>[0]["readClipboardText"];
|
||||
showMpvOsd: Parameters<typeof updateLastCardFromClipboardService>[0]["showMpvOsd"];
|
||||
},
|
||||
): Parameters<typeof updateLastCardFromClipboardService>[0] {
|
||||
return {
|
||||
ankiIntegration: options.ankiIntegration,
|
||||
readClipboardText: options.readClipboardText,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTriggerFieldGroupingDepsRuntimeService(
|
||||
options: {
|
||||
ankiIntegration: Parameters<typeof triggerFieldGroupingService>[0]["ankiIntegration"];
|
||||
showMpvOsd: Parameters<typeof triggerFieldGroupingService>[0]["showMpvOsd"];
|
||||
},
|
||||
): Parameters<typeof triggerFieldGroupingService>[0] {
|
||||
return {
|
||||
ankiIntegration: options.ankiIntegration,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMarkLastCardAsAudioCardDepsRuntimeService(
|
||||
options: {
|
||||
ankiIntegration: Parameters<typeof markLastCardAsAudioCardService>[0]["ankiIntegration"];
|
||||
showMpvOsd: Parameters<typeof markLastCardAsAudioCardService>[0]["showMpvOsd"];
|
||||
},
|
||||
): Parameters<typeof markLastCardAsAudioCardService>[0] {
|
||||
return {
|
||||
ankiIntegration: options.ankiIntegration,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMineSentenceCardDepsRuntimeService(
|
||||
options: {
|
||||
ankiIntegration: Parameters<typeof mineSentenceCardService>[0]["ankiIntegration"];
|
||||
mpvClient: Parameters<typeof mineSentenceCardService>[0]["mpvClient"];
|
||||
showMpvOsd: Parameters<typeof mineSentenceCardService>[0]["showMpvOsd"];
|
||||
},
|
||||
): Parameters<typeof mineSentenceCardService>[0] {
|
||||
return {
|
||||
ankiIntegration: options.ankiIntegration,
|
||||
mpvClient: options.mpvClient,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function createHandleMineSentenceDigitDepsRuntimeService(
|
||||
options: {
|
||||
subtitleTimingTracker: Parameters<typeof handleMineSentenceDigitService>[1]["subtitleTimingTracker"];
|
||||
ankiIntegration: Parameters<typeof handleMineSentenceDigitService>[1]["ankiIntegration"];
|
||||
getCurrentSecondarySubText: Parameters<typeof handleMineSentenceDigitService>[1]["getCurrentSecondarySubText"];
|
||||
showMpvOsd: Parameters<typeof handleMineSentenceDigitService>[1]["showMpvOsd"];
|
||||
logError: Parameters<typeof handleMineSentenceDigitService>[1]["logError"];
|
||||
},
|
||||
): Parameters<typeof handleMineSentenceDigitService>[1] {
|
||||
return {
|
||||
subtitleTimingTracker: options.subtitleTimingTracker,
|
||||
ankiIntegration: options.ankiIntegration,
|
||||
getCurrentSecondarySubText: options.getCurrentSecondarySubText,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
logError: options.logError,
|
||||
};
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createMpvIpcClientDepsRuntimeService } from "./mpv-client-deps-runtime-service";
|
||||
|
||||
test("createMpvIpcClientDepsRuntimeService returns passthrough dep object", async () => {
|
||||
const marker = {
|
||||
getResolvedConfig: () => ({ auto_start_overlay: false } as never),
|
||||
autoStartOverlay: true,
|
||||
setOverlayVisible: () => {},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
getCurrentSubText: () => "x",
|
||||
setCurrentSubText: () => {},
|
||||
setCurrentSubAssText: () => {},
|
||||
getSubtitleTimingTracker: () => null,
|
||||
subtitleWsBroadcast: () => {},
|
||||
getOverlayWindowsCount: () => 0,
|
||||
tokenizeSubtitle: async () => ({ text: "x", tokens: [], mergedTokens: [] }),
|
||||
broadcastToOverlayWindows: () => {},
|
||||
updateCurrentMediaPath: () => {},
|
||||
updateMpvSubtitleRenderMetrics: () => {},
|
||||
getMpvSubtitleRenderMetrics: () => ({
|
||||
subPos: 100,
|
||||
subFontSize: 40,
|
||||
subScale: 1,
|
||||
subMarginY: 0,
|
||||
subMarginX: 0,
|
||||
subFont: "sans",
|
||||
subSpacing: 0,
|
||||
subBold: false,
|
||||
subItalic: false,
|
||||
subBorderSize: 0,
|
||||
subShadowOffset: 0,
|
||||
subAssOverride: "yes",
|
||||
subScaleByWindow: true,
|
||||
subUseMargins: true,
|
||||
osdHeight: 720,
|
||||
osdDimensions: null,
|
||||
}),
|
||||
setPreviousSecondarySubVisibility: () => {},
|
||||
showMpvOsd: () => {},
|
||||
};
|
||||
|
||||
const deps = createMpvIpcClientDepsRuntimeService(marker);
|
||||
assert.equal(deps.autoStartOverlay, true);
|
||||
assert.equal(deps.getCurrentSubText(), "x");
|
||||
assert.equal(deps.getOverlayWindowsCount(), 0);
|
||||
assert.equal(deps.shouldBindVisibleOverlayToMpvSubVisibility(), true);
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
MpvIpcClientDeps,
|
||||
} from "./mpv-service";
|
||||
import { Config, MpvSubtitleRenderMetrics, SubtitleData } from "../../types";
|
||||
|
||||
interface SubtitleTimingTrackerLike {
|
||||
recordSubtitle: (text: string, start: number, end: number) => void;
|
||||
}
|
||||
|
||||
export interface MpvClientDepsRuntimeOptions {
|
||||
getResolvedConfig: () => Config;
|
||||
autoStartOverlay: boolean;
|
||||
setOverlayVisible: (visible: boolean) => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
|
||||
getCurrentSubText: () => string;
|
||||
setCurrentSubText: (text: string) => void;
|
||||
setCurrentSubAssText: (text: string) => void;
|
||||
getSubtitleTimingTracker: () => SubtitleTimingTrackerLike | null;
|
||||
subtitleWsBroadcast: (text: string) => void;
|
||||
getOverlayWindowsCount: () => number;
|
||||
tokenizeSubtitle: (text: string) => Promise<SubtitleData>;
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||
updateCurrentMediaPath: (mediaPath: unknown) => void;
|
||||
updateMpvSubtitleRenderMetrics: (
|
||||
patch: Partial<MpvSubtitleRenderMetrics>,
|
||||
) => void;
|
||||
getMpvSubtitleRenderMetrics: () => MpvSubtitleRenderMetrics;
|
||||
setPreviousSecondarySubVisibility: (value: boolean | null) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}
|
||||
|
||||
export function createMpvIpcClientDepsRuntimeService(
|
||||
options: MpvClientDepsRuntimeOptions,
|
||||
): MpvIpcClientDeps {
|
||||
return {
|
||||
getResolvedConfig: options.getResolvedConfig,
|
||||
autoStartOverlay: options.autoStartOverlay,
|
||||
setOverlayVisible: options.setOverlayVisible,
|
||||
shouldBindVisibleOverlayToMpvSubVisibility:
|
||||
options.shouldBindVisibleOverlayToMpvSubVisibility,
|
||||
isVisibleOverlayVisible: options.isVisibleOverlayVisible,
|
||||
getReconnectTimer: options.getReconnectTimer,
|
||||
setReconnectTimer: options.setReconnectTimer,
|
||||
getCurrentSubText: options.getCurrentSubText,
|
||||
setCurrentSubText: options.setCurrentSubText,
|
||||
setCurrentSubAssText: options.setCurrentSubAssText,
|
||||
getSubtitleTimingTracker: options.getSubtitleTimingTracker,
|
||||
subtitleWsBroadcast: options.subtitleWsBroadcast,
|
||||
getOverlayWindowsCount: options.getOverlayWindowsCount,
|
||||
tokenizeSubtitle: options.tokenizeSubtitle,
|
||||
broadcastToOverlayWindows: options.broadcastToOverlayWindows,
|
||||
updateCurrentMediaPath: options.updateCurrentMediaPath,
|
||||
updateMpvSubtitleRenderMetrics: options.updateMpvSubtitleRenderMetrics,
|
||||
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
|
||||
setPreviousSecondarySubVisibility: options.setPreviousSecondarySubVisibility,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService,
|
||||
createOverlayWindowRuntimeDepsService,
|
||||
createVisibleOverlayVisibilityDepsRuntimeService,
|
||||
} from "./overlay-runtime-deps-service";
|
||||
} from "./overlay-deps-runtime-service";
|
||||
|
||||
test("createOverlayWindowRuntimeDepsService maps runtime state providers", () => {
|
||||
let visible = true;
|
||||
42
src/core/services/overlay-manager-service.test.ts
Normal file
42
src/core/services/overlay-manager-service.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createOverlayManagerService } from "./overlay-manager-service";
|
||||
|
||||
test("overlay manager initializes with empty windows and hidden overlays", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
assert.equal(manager.getMainWindow(), null);
|
||||
assert.equal(manager.getInvisibleWindow(), null);
|
||||
assert.equal(manager.getVisibleOverlayVisible(), false);
|
||||
assert.equal(manager.getInvisibleOverlayVisible(), false);
|
||||
assert.deepEqual(manager.getOverlayWindows(), []);
|
||||
});
|
||||
|
||||
test("overlay manager stores window references and returns stable window order", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
const visibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
|
||||
const invisibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
|
||||
|
||||
manager.setMainWindow(visibleWindow);
|
||||
manager.setInvisibleWindow(invisibleWindow);
|
||||
|
||||
assert.equal(manager.getMainWindow(), visibleWindow);
|
||||
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
|
||||
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow]);
|
||||
});
|
||||
|
||||
test("overlay manager excludes destroyed windows", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
manager.setMainWindow({ isDestroyed: () => true } as unknown as Electron.BrowserWindow);
|
||||
manager.setInvisibleWindow({ isDestroyed: () => false } as unknown as Electron.BrowserWindow);
|
||||
|
||||
assert.equal(manager.getOverlayWindows().length, 1);
|
||||
});
|
||||
|
||||
test("overlay manager stores visibility state", () => {
|
||||
const manager = createOverlayManagerService();
|
||||
|
||||
manager.setVisibleOverlayVisible(true);
|
||||
manager.setInvisibleOverlayVisible(true);
|
||||
assert.equal(manager.getVisibleOverlayVisible(), true);
|
||||
assert.equal(manager.getInvisibleOverlayVisible(), true);
|
||||
});
|
||||
49
src/core/services/overlay-manager-service.ts
Normal file
49
src/core/services/overlay-manager-service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
export interface OverlayManagerService {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
setMainWindow: (window: BrowserWindow | null) => void;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
}
|
||||
|
||||
export function createOverlayManagerService(): OverlayManagerService {
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let invisibleWindow: BrowserWindow | null = null;
|
||||
let visibleOverlayVisible = false;
|
||||
let invisibleOverlayVisible = false;
|
||||
|
||||
return {
|
||||
getMainWindow: () => mainWindow,
|
||||
setMainWindow: (window) => {
|
||||
mainWindow = window;
|
||||
},
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
setInvisibleWindow: (window) => {
|
||||
invisibleWindow = window;
|
||||
},
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
setVisibleOverlayVisible: (visible) => {
|
||||
visibleOverlayVisible = visible;
|
||||
},
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
setInvisibleOverlayVisible: (visible) => {
|
||||
invisibleOverlayVisible = visible;
|
||||
},
|
||||
getOverlayWindows: () => {
|
||||
const windows: BrowserWindow[] = [];
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
windows.push(mainWindow);
|
||||
}
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
windows.push(invisibleWindow);
|
||||
}
|
||||
return windows;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createOverlayShortcutLifecycleDepsRuntimeService,
|
||||
createOverlayShortcutRuntimeDepsService,
|
||||
} from "./overlay-shortcut-runtime-deps-service";
|
||||
|
||||
test("createOverlayShortcutRuntimeDepsService returns callable runtime deps", async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createOverlayShortcutRuntimeDepsService({
|
||||
showMpvOsd: () => calls.push("showMpvOsd"),
|
||||
openRuntimeOptions: () => calls.push("openRuntimeOptions"),
|
||||
openJimaku: () => calls.push("openJimaku"),
|
||||
markAudioCard: async () => {
|
||||
calls.push("markAudioCard");
|
||||
},
|
||||
copySubtitleMultiple: () => calls.push("copySubtitleMultiple"),
|
||||
copySubtitle: () => calls.push("copySubtitle"),
|
||||
toggleSecondarySub: () => calls.push("toggleSecondarySub"),
|
||||
updateLastCardFromClipboard: async () => {
|
||||
calls.push("updateLastCardFromClipboard");
|
||||
},
|
||||
triggerFieldGrouping: async () => {
|
||||
calls.push("triggerFieldGrouping");
|
||||
},
|
||||
triggerSubsync: async () => {
|
||||
calls.push("triggerSubsync");
|
||||
},
|
||||
mineSentence: async () => {
|
||||
calls.push("mineSentence");
|
||||
},
|
||||
mineSentenceMultiple: () => calls.push("mineSentenceMultiple"),
|
||||
});
|
||||
|
||||
deps.copySubtitle();
|
||||
await deps.mineSentence();
|
||||
deps.mineSentenceMultiple(2);
|
||||
|
||||
assert.deepEqual(calls, ["copySubtitle", "mineSentence", "mineSentenceMultiple"]);
|
||||
});
|
||||
|
||||
test("createOverlayShortcutLifecycleDepsRuntimeService returns lifecycle passthrough", () => {
|
||||
const deps = createOverlayShortcutLifecycleDepsRuntimeService({
|
||||
getConfiguredShortcuts: () => ({ actions: [] } as never),
|
||||
getOverlayHandlers: () => ({} as never),
|
||||
cancelPendingMultiCopy: () => {},
|
||||
cancelPendingMineSentenceMultiple: () => {},
|
||||
});
|
||||
|
||||
assert.ok(deps.getConfiguredShortcuts());
|
||||
assert.ok(deps.getOverlayHandlers());
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
OverlayShortcutLifecycleDeps,
|
||||
} from "./overlay-shortcut-lifecycle-service";
|
||||
import {
|
||||
OverlayShortcutRuntimeDeps,
|
||||
} from "./overlay-shortcut-runtime-service";
|
||||
|
||||
export interface OverlayShortcutRuntimeDepsOptions {
|
||||
showMpvOsd: (text: string) => void;
|
||||
openRuntimeOptions: () => void;
|
||||
openJimaku: () => void;
|
||||
markAudioCard: () => Promise<void>;
|
||||
copySubtitleMultiple: (timeoutMs: number) => void;
|
||||
copySubtitle: () => void;
|
||||
toggleSecondarySub: () => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsync: () => Promise<void>;
|
||||
mineSentence: () => Promise<void>;
|
||||
mineSentenceMultiple: (timeoutMs: number) => void;
|
||||
}
|
||||
|
||||
export interface OverlayShortcutLifecycleDepsOptions {
|
||||
getConfiguredShortcuts: OverlayShortcutLifecycleDeps["getConfiguredShortcuts"];
|
||||
getOverlayHandlers: OverlayShortcutLifecycleDeps["getOverlayHandlers"];
|
||||
cancelPendingMultiCopy: () => void;
|
||||
cancelPendingMineSentenceMultiple: () => void;
|
||||
}
|
||||
|
||||
export function createOverlayShortcutRuntimeDepsService(
|
||||
options: OverlayShortcutRuntimeDepsOptions,
|
||||
): OverlayShortcutRuntimeDeps {
|
||||
return {
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
openRuntimeOptions: options.openRuntimeOptions,
|
||||
openJimaku: options.openJimaku,
|
||||
markAudioCard: options.markAudioCard,
|
||||
copySubtitleMultiple: options.copySubtitleMultiple,
|
||||
copySubtitle: options.copySubtitle,
|
||||
toggleSecondarySub: options.toggleSecondarySub,
|
||||
updateLastCardFromClipboard: options.updateLastCardFromClipboard,
|
||||
triggerFieldGrouping: options.triggerFieldGrouping,
|
||||
triggerSubsync: options.triggerSubsync,
|
||||
mineSentence: options.mineSentence,
|
||||
mineSentenceMultiple: options.mineSentenceMultiple,
|
||||
};
|
||||
}
|
||||
|
||||
export function createOverlayShortcutLifecycleDepsRuntimeService(
|
||||
options: OverlayShortcutLifecycleDepsOptions,
|
||||
): OverlayShortcutLifecycleDeps {
|
||||
return {
|
||||
getConfiguredShortcuts: options.getConfiguredShortcuts,
|
||||
getOverlayHandlers: options.getOverlayHandlers,
|
||||
cancelPendingMultiCopy: options.cancelPendingMultiCopy,
|
||||
cancelPendingMineSentenceMultiple:
|
||||
options.cancelPendingMineSentenceMultiple,
|
||||
};
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createOverlayVisibilityFacadeDepsRuntimeService } from "./overlay-visibility-facade-deps-runtime-service";
|
||||
|
||||
test("createOverlayVisibilityFacadeDepsRuntimeService returns working deps object", () => {
|
||||
let visible = false;
|
||||
let invisible = true;
|
||||
let mpvSubVisible: boolean | null = null;
|
||||
let syncCalls = 0;
|
||||
|
||||
const deps = createOverlayVisibilityFacadeDepsRuntimeService({
|
||||
getVisibleOverlayVisible: () => visible,
|
||||
getInvisibleOverlayVisible: () => invisible,
|
||||
setVisibleOverlayVisibleState: (nextVisible) => {
|
||||
visible = nextVisible;
|
||||
},
|
||||
setInvisibleOverlayVisibleState: (nextVisible) => {
|
||||
invisible = nextVisible;
|
||||
},
|
||||
updateVisibleOverlayVisibility: () => {},
|
||||
updateInvisibleOverlayVisibility: () => {},
|
||||
syncInvisibleOverlayMousePassthrough: () => {
|
||||
syncCalls += 1;
|
||||
},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
isMpvConnected: () => true,
|
||||
setMpvSubVisibility: (nextVisible) => {
|
||||
mpvSubVisible = nextVisible;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(deps.getVisibleOverlayVisible(), false);
|
||||
assert.equal(deps.getInvisibleOverlayVisible(), true);
|
||||
|
||||
deps.setVisibleOverlayVisibleState(true);
|
||||
deps.setInvisibleOverlayVisibleState(false);
|
||||
deps.syncInvisibleOverlayMousePassthrough();
|
||||
deps.setMpvSubVisibility(false);
|
||||
|
||||
assert.equal(visible, true);
|
||||
assert.equal(invisible, false);
|
||||
assert.equal(syncCalls, 1);
|
||||
assert.equal(mpvSubVisible, false);
|
||||
assert.equal(deps.shouldBindVisibleOverlayToMpvSubVisibility(), true);
|
||||
assert.equal(deps.isMpvConnected(), true);
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import {
|
||||
OverlayVisibilityFacadeDeps,
|
||||
} from "./overlay-visibility-facade-service";
|
||||
|
||||
export interface OverlayVisibilityFacadeDepsRuntimeOptions {
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
setVisibleOverlayVisibleState: (nextVisible: boolean) => void;
|
||||
setInvisibleOverlayVisibleState: (nextVisible: boolean) => void;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncInvisibleOverlayMousePassthrough: () => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
isMpvConnected: () => boolean;
|
||||
setMpvSubVisibility: (mpvSubVisible: boolean) => void;
|
||||
}
|
||||
|
||||
export function createOverlayVisibilityFacadeDepsRuntimeService(
|
||||
options: OverlayVisibilityFacadeDepsRuntimeOptions,
|
||||
): OverlayVisibilityFacadeDeps {
|
||||
return {
|
||||
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
|
||||
setVisibleOverlayVisibleState: options.setVisibleOverlayVisibleState,
|
||||
setInvisibleOverlayVisibleState: options.setInvisibleOverlayVisibleState,
|
||||
updateVisibleOverlayVisibility: options.updateVisibleOverlayVisibility,
|
||||
updateInvisibleOverlayVisibility: options.updateInvisibleOverlayVisibility,
|
||||
syncInvisibleOverlayMousePassthrough:
|
||||
options.syncInvisibleOverlayMousePassthrough,
|
||||
shouldBindVisibleOverlayToMpvSubVisibility:
|
||||
options.shouldBindVisibleOverlayToMpvSubVisibility,
|
||||
isMpvConnected: options.isMpvConnected,
|
||||
setMpvSubVisibility: options.setMpvSubVisibility,
|
||||
};
|
||||
}
|
||||
@@ -1,35 +1,11 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createGlobalShortcutRegistrationDepsRuntimeService,
|
||||
createSecondarySubtitleCycleDepsRuntimeService,
|
||||
createYomitanSettingsWindowDepsRuntimeService,
|
||||
runOverlayShortcutLocalFallbackRuntimeService,
|
||||
} from "./shortcut-ui-runtime-deps-service";
|
||||
} from "./shortcut-ui-deps-runtime-service";
|
||||
|
||||
function makeOptions() {
|
||||
return {
|
||||
yomitanExt: null,
|
||||
getYomitanSettingsWindow: () => null,
|
||||
setYomitanSettingsWindow: () => {},
|
||||
|
||||
shortcuts: {
|
||||
toggleVisibleOverlayGlobal: "Ctrl+Shift+O",
|
||||
toggleInvisibleOverlayGlobal: "Ctrl+Alt+O",
|
||||
},
|
||||
onToggleVisibleOverlay: () => {},
|
||||
onToggleInvisibleOverlay: () => {},
|
||||
onOpenYomitanSettings: () => {},
|
||||
isDev: false,
|
||||
getMainWindow: () => null,
|
||||
|
||||
getSecondarySubMode: () => "hover" as const,
|
||||
setSecondarySubMode: () => {},
|
||||
getLastSecondarySubToggleAtMs: () => 0,
|
||||
setLastSecondarySubToggleAtMs: () => {},
|
||||
broadcastSecondarySubMode: () => {},
|
||||
showMpvOsd: () => {},
|
||||
|
||||
getConfiguredShortcuts: () => ({
|
||||
toggleVisibleOverlayGlobal: null,
|
||||
toggleInvisibleOverlayGlobal: null,
|
||||
@@ -63,17 +39,6 @@ function makeOptions() {
|
||||
};
|
||||
}
|
||||
|
||||
test("shortcut ui deps builders return expected adapters", () => {
|
||||
const options = makeOptions();
|
||||
const yomitan = createYomitanSettingsWindowDepsRuntimeService(options);
|
||||
const globalShortcuts = createGlobalShortcutRegistrationDepsRuntimeService(options);
|
||||
const secondary = createSecondarySubtitleCycleDepsRuntimeService(options);
|
||||
|
||||
assert.equal(yomitan.yomitanExt, null);
|
||||
assert.equal(typeof globalShortcuts.onOpenYomitanSettings, "function");
|
||||
assert.equal(secondary.getSecondarySubMode(), "hover");
|
||||
});
|
||||
|
||||
test("runOverlayShortcutLocalFallbackRuntimeService delegates and returns boolean", () => {
|
||||
const options = {
|
||||
...makeOptions(),
|
||||
24
src/core/services/shortcut-ui-deps-runtime-service.ts
Normal file
24
src/core/services/shortcut-ui-deps-runtime-service.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner";
|
||||
|
||||
export interface ShortcutUiRuntimeDepsOptions {
|
||||
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||
getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers;
|
||||
shortcutMatcher: (
|
||||
input: Electron.Input,
|
||||
accelerator: string,
|
||||
allowWhenRegistered?: boolean,
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export function runOverlayShortcutLocalFallbackRuntimeService(
|
||||
input: Electron.Input,
|
||||
options: ShortcutUiRuntimeDepsOptions,
|
||||
): boolean {
|
||||
return runOverlayShortcutLocalFallback(
|
||||
input,
|
||||
options.getConfiguredShortcuts(),
|
||||
options.shortcutMatcher,
|
||||
options.getOverlayShortcutFallbackHandlers(),
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Extension } from "electron";
|
||||
import { SecondarySubMode } from "../../types";
|
||||
import { ConfiguredShortcuts } from "../utils/shortcut-config";
|
||||
import { CycleSecondarySubModeDeps } from "./secondary-subtitle-service";
|
||||
import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner";
|
||||
import { OpenYomitanSettingsWindowOptions } from "./yomitan-settings-service";
|
||||
import { RegisterGlobalShortcutsServiceOptions } from "./shortcut-service";
|
||||
|
||||
export interface ShortcutUiRuntimeDepsOptions {
|
||||
yomitanExt: Extension | null;
|
||||
getYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["getExistingWindow"];
|
||||
setYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["setWindow"];
|
||||
|
||||
shortcuts: RegisterGlobalShortcutsServiceOptions["shortcuts"];
|
||||
onToggleVisibleOverlay: () => void;
|
||||
onToggleInvisibleOverlay: () => void;
|
||||
onOpenYomitanSettings: () => void;
|
||||
isDev: boolean;
|
||||
getMainWindow: RegisterGlobalShortcutsServiceOptions["getMainWindow"];
|
||||
|
||||
getSecondarySubMode: () => SecondarySubMode;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
getLastSecondarySubToggleAtMs: () => number;
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
|
||||
getConfiguredShortcuts: () => ConfiguredShortcuts;
|
||||
getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers;
|
||||
shortcutMatcher: (
|
||||
input: Electron.Input,
|
||||
accelerator: string,
|
||||
allowWhenRegistered?: boolean,
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export function createYomitanSettingsWindowDepsRuntimeService(
|
||||
options: ShortcutUiRuntimeDepsOptions,
|
||||
): OpenYomitanSettingsWindowOptions {
|
||||
return {
|
||||
yomitanExt: options.yomitanExt,
|
||||
getExistingWindow: options.getYomitanSettingsWindow,
|
||||
setWindow: options.setYomitanSettingsWindow,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGlobalShortcutRegistrationDepsRuntimeService(
|
||||
options: ShortcutUiRuntimeDepsOptions,
|
||||
): RegisterGlobalShortcutsServiceOptions {
|
||||
return {
|
||||
shortcuts: options.shortcuts,
|
||||
onToggleVisibleOverlay: options.onToggleVisibleOverlay,
|
||||
onToggleInvisibleOverlay: options.onToggleInvisibleOverlay,
|
||||
onOpenYomitanSettings: options.onOpenYomitanSettings,
|
||||
isDev: options.isDev,
|
||||
getMainWindow: options.getMainWindow,
|
||||
};
|
||||
}
|
||||
|
||||
export function createSecondarySubtitleCycleDepsRuntimeService(
|
||||
options: ShortcutUiRuntimeDepsOptions,
|
||||
): CycleSecondarySubModeDeps {
|
||||
return {
|
||||
getSecondarySubMode: options.getSecondarySubMode,
|
||||
setSecondarySubMode: options.setSecondarySubMode,
|
||||
getLastSecondarySubToggleAtMs: options.getLastSecondarySubToggleAtMs,
|
||||
setLastSecondarySubToggleAtMs: options.setLastSecondarySubToggleAtMs,
|
||||
broadcastSecondarySubMode: options.broadcastSecondarySubMode,
|
||||
showMpvOsd: options.showMpvOsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function runOverlayShortcutLocalFallbackRuntimeService(
|
||||
input: Electron.Input,
|
||||
options: ShortcutUiRuntimeDepsOptions,
|
||||
): boolean {
|
||||
return runOverlayShortcutLocalFallback(
|
||||
input,
|
||||
options.getConfiguredShortcuts(),
|
||||
options.shortcutMatcher,
|
||||
options.getOverlayShortcutFallbackHandlers(),
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,6 @@ import {
|
||||
AppShutdownRuntimeDeps,
|
||||
runAppShutdownRuntimeService,
|
||||
} from "./app-shutdown-runtime-service";
|
||||
import {
|
||||
createStartupAppReadyDepsRuntimeService,
|
||||
createStartupAppShutdownDepsRuntimeService,
|
||||
} from "./startup-lifecycle-runtime-deps-service";
|
||||
|
||||
type StartupLifecycleHookDeps = Pick<
|
||||
AppLifecycleDepsRuntimeOptions,
|
||||
@@ -29,14 +25,10 @@ export function createStartupLifecycleHooksRuntimeService(
|
||||
): StartupLifecycleHookDeps {
|
||||
return {
|
||||
onReady: async () => {
|
||||
await runAppReadyRuntimeService(
|
||||
createStartupAppReadyDepsRuntimeService(options.appReadyDeps),
|
||||
);
|
||||
await runAppReadyRuntimeService(options.appReadyDeps);
|
||||
},
|
||||
onWillQuitCleanup: () => {
|
||||
runAppShutdownRuntimeService(
|
||||
createStartupAppShutdownDepsRuntimeService(options.appShutdownDeps),
|
||||
);
|
||||
runAppShutdownRuntimeService(options.appShutdownDeps);
|
||||
},
|
||||
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: options.restoreWindowsOnActivate,
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createStartupAppReadyDepsRuntimeService,
|
||||
createStartupAppShutdownDepsRuntimeService,
|
||||
} from "./startup-lifecycle-runtime-deps-service";
|
||||
|
||||
test("createStartupAppReadyDepsRuntimeService preserves runtime deps behavior", async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createStartupAppReadyDepsRuntimeService({
|
||||
loadSubtitlePosition: () => calls.push("loadSubtitlePosition"),
|
||||
resolveKeybindings: () => calls.push("resolveKeybindings"),
|
||||
createMpvClient: () => calls.push("createMpvClient"),
|
||||
reloadConfig: () => calls.push("reloadConfig"),
|
||||
getResolvedConfig: () => ({
|
||||
secondarySub: { defaultMode: "hover" },
|
||||
websocket: { enabled: "auto", port: 1234 },
|
||||
}),
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => {},
|
||||
initRuntimeOptionsManager: () => calls.push("initRuntimeOptionsManager"),
|
||||
setSecondarySubMode: () => calls.push("setSecondarySubMode"),
|
||||
defaultSecondarySubMode: "hover",
|
||||
defaultWebsocketPort: 8765,
|
||||
hasMpvWebsocketPlugin: () => true,
|
||||
startSubtitleWebsocket: () => calls.push("startSubtitleWebsocket"),
|
||||
log: () => calls.push("log"),
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
calls.push("createMecab");
|
||||
},
|
||||
createSubtitleTimingTracker: () => calls.push("createSubtitleTimingTracker"),
|
||||
loadYomitanExtension: async () => {
|
||||
calls.push("loadYomitan");
|
||||
},
|
||||
texthookerOnlyMode: false,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||
initializeOverlayRuntime: () => calls.push("initOverlayRuntime"),
|
||||
handleInitialArgs: () => calls.push("handleInitialArgs"),
|
||||
});
|
||||
|
||||
deps.loadSubtitlePosition();
|
||||
await deps.createMecabTokenizerAndCheck();
|
||||
deps.handleInitialArgs();
|
||||
|
||||
assert.equal(deps.defaultWebsocketPort, 8765);
|
||||
assert.equal(deps.defaultSecondarySubMode, "hover");
|
||||
assert.deepEqual(calls, ["loadSubtitlePosition", "createMecab", "handleInitialArgs"]);
|
||||
});
|
||||
|
||||
test("createStartupAppShutdownDepsRuntimeService preserves shutdown handlers", () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createStartupAppShutdownDepsRuntimeService({
|
||||
unregisterAllGlobalShortcuts: () => calls.push("unregisterAllGlobalShortcuts"),
|
||||
stopSubtitleWebsocket: () => calls.push("stopSubtitleWebsocket"),
|
||||
stopTexthookerService: () => calls.push("stopTexthookerService"),
|
||||
destroyYomitanParserWindow: () => calls.push("destroyYomitanParserWindow"),
|
||||
clearYomitanParserPromises: () => calls.push("clearYomitanParserPromises"),
|
||||
stopWindowTracker: () => calls.push("stopWindowTracker"),
|
||||
destroyMpvSocket: () => calls.push("destroyMpvSocket"),
|
||||
clearReconnectTimer: () => calls.push("clearReconnectTimer"),
|
||||
destroySubtitleTimingTracker: () => calls.push("destroySubtitleTimingTracker"),
|
||||
destroyAnkiIntegration: () => calls.push("destroyAnkiIntegration"),
|
||||
});
|
||||
|
||||
deps.stopSubtitleWebsocket();
|
||||
deps.clearReconnectTimer();
|
||||
deps.destroyAnkiIntegration();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"stopSubtitleWebsocket",
|
||||
"clearReconnectTimer",
|
||||
"destroyAnkiIntegration",
|
||||
]);
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import {
|
||||
AppReadyRuntimeDeps,
|
||||
} from "./app-ready-runtime-service";
|
||||
import {
|
||||
AppShutdownRuntimeDeps,
|
||||
} from "./app-shutdown-runtime-service";
|
||||
|
||||
export type StartupAppReadyDepsRuntimeOptions = AppReadyRuntimeDeps;
|
||||
export type StartupAppShutdownDepsRuntimeOptions = AppShutdownRuntimeDeps;
|
||||
|
||||
export function createStartupAppReadyDepsRuntimeService(
|
||||
options: StartupAppReadyDepsRuntimeOptions,
|
||||
): AppReadyRuntimeDeps {
|
||||
return {
|
||||
loadSubtitlePosition: options.loadSubtitlePosition,
|
||||
resolveKeybindings: options.resolveKeybindings,
|
||||
createMpvClient: options.createMpvClient,
|
||||
reloadConfig: options.reloadConfig,
|
||||
getResolvedConfig: options.getResolvedConfig,
|
||||
getConfigWarnings: options.getConfigWarnings,
|
||||
logConfigWarning: options.logConfigWarning,
|
||||
initRuntimeOptionsManager: options.initRuntimeOptionsManager,
|
||||
setSecondarySubMode: options.setSecondarySubMode,
|
||||
defaultSecondarySubMode: options.defaultSecondarySubMode,
|
||||
defaultWebsocketPort: options.defaultWebsocketPort,
|
||||
hasMpvWebsocketPlugin: options.hasMpvWebsocketPlugin,
|
||||
startSubtitleWebsocket: options.startSubtitleWebsocket,
|
||||
log: options.log,
|
||||
createMecabTokenizerAndCheck: options.createMecabTokenizerAndCheck,
|
||||
createSubtitleTimingTracker: options.createSubtitleTimingTracker,
|
||||
loadYomitanExtension: options.loadYomitanExtension,
|
||||
texthookerOnlyMode: options.texthookerOnlyMode,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig:
|
||||
options.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
initializeOverlayRuntime: options.initializeOverlayRuntime,
|
||||
handleInitialArgs: options.handleInitialArgs,
|
||||
};
|
||||
}
|
||||
|
||||
export function createStartupAppShutdownDepsRuntimeService(
|
||||
options: StartupAppShutdownDepsRuntimeOptions,
|
||||
): AppShutdownRuntimeDeps {
|
||||
return {
|
||||
unregisterAllGlobalShortcuts: options.unregisterAllGlobalShortcuts,
|
||||
stopSubtitleWebsocket: options.stopSubtitleWebsocket,
|
||||
stopTexthookerService: options.stopTexthookerService,
|
||||
destroyYomitanParserWindow: options.destroyYomitanParserWindow,
|
||||
clearYomitanParserPromises: options.clearYomitanParserPromises,
|
||||
stopWindowTracker: options.stopWindowTracker,
|
||||
destroyMpvSocket: options.destroyMpvSocket,
|
||||
clearReconnectTimer: options.clearReconnectTimer,
|
||||
destroySubtitleTimingTracker: options.destroySubtitleTimingTracker,
|
||||
destroyAnkiIntegration: options.destroyAnkiIntegration,
|
||||
};
|
||||
}
|
||||
289
src/main.ts
289
src/main.ts
@@ -22,7 +22,6 @@ import {
|
||||
clipboard,
|
||||
shell,
|
||||
protocol,
|
||||
screen,
|
||||
Extension,
|
||||
} from "electron";
|
||||
|
||||
@@ -40,18 +39,12 @@ protocol.registerSchemesAsPrivileged([
|
||||
]);
|
||||
|
||||
import * as path from "path";
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
import * as os from "os";
|
||||
import * as fs from "fs";
|
||||
import * as crypto from "crypto";
|
||||
import { MecabTokenizer } from "./mecab-tokenizer";
|
||||
import { BaseWindowTracker } from "./window-trackers";
|
||||
import type {
|
||||
JimakuApiResponse,
|
||||
JimakuDownloadResult,
|
||||
JimakuMediaInfo,
|
||||
JimakuConfig,
|
||||
JimakuLanguagePreference,
|
||||
SubtitleData,
|
||||
SubtitlePosition,
|
||||
@@ -78,16 +71,12 @@ import {
|
||||
getSubsyncConfig,
|
||||
} from "./subsync/utils";
|
||||
import {
|
||||
hasExplicitCommand,
|
||||
parseArgs,
|
||||
shouldStartApp,
|
||||
} from "./cli/args";
|
||||
import type { CliArgs, CliCommandSource } from "./cli/args";
|
||||
import { printHelp } from "./cli/help";
|
||||
import {
|
||||
asBoolean,
|
||||
asFiniteNumber,
|
||||
asString,
|
||||
enforceUnsupportedWaylandMode,
|
||||
forceX11Backend,
|
||||
generateDefaultConfigFile,
|
||||
@@ -104,48 +93,33 @@ import {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
broadcastToOverlayWindowsRuntimeService,
|
||||
copyCurrentSubtitleService,
|
||||
createAnkiJimakuIpcDepsRuntimeService,
|
||||
createAppLifecycleDepsRuntimeService,
|
||||
createAppLoggingRuntimeService,
|
||||
createCliCommandDepsRuntimeService,
|
||||
createCopyCurrentSubtitleDepsRuntimeService,
|
||||
createOverlayManagerService,
|
||||
createFieldGroupingOverlayRuntimeService,
|
||||
createGlobalShortcutRegistrationDepsRuntimeService,
|
||||
createHandleMineSentenceDigitDepsRuntimeService,
|
||||
createHandleMultiCopyDigitDepsRuntimeService,
|
||||
createInitializeOverlayRuntimeDepsService,
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService,
|
||||
createIpcDepsRuntimeService,
|
||||
createMarkLastCardAsAudioCardDepsRuntimeService,
|
||||
createMecabTokenizerAndCheckRuntimeService,
|
||||
createMineSentenceCardDepsRuntimeService,
|
||||
createMpvCommandIpcDepsRuntimeService,
|
||||
createMpvIpcClientDepsRuntimeService,
|
||||
createNumericShortcutRuntimeService,
|
||||
createOverlayShortcutLifecycleDepsRuntimeService,
|
||||
createOverlayShortcutRuntimeDepsService,
|
||||
createOverlayShortcutRuntimeHandlers,
|
||||
createOverlayVisibilityFacadeDepsRuntimeService,
|
||||
createOverlayWindowRuntimeDepsService,
|
||||
createOverlayWindowService,
|
||||
createRuntimeOptionsIpcDepsRuntimeService,
|
||||
createRuntimeOptionsManagerRuntimeService,
|
||||
createSecondarySubtitleCycleDepsRuntimeService,
|
||||
createStartupLifecycleHooksRuntimeService,
|
||||
createSubsyncRuntimeDepsService,
|
||||
createSubtitleTimingTrackerRuntimeService,
|
||||
createTokenizerDepsRuntimeService,
|
||||
createTriggerFieldGroupingDepsRuntimeService,
|
||||
createUpdateLastCardFromClipboardDepsRuntimeService,
|
||||
createVisibleOverlayVisibilityDepsRuntimeService,
|
||||
createYomitanSettingsWindowDepsRuntimeService,
|
||||
cycleSecondarySubModeService,
|
||||
enforceOverlayLayerOrderService,
|
||||
ensureOverlayWindowLevelService,
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
getJimakuLanguagePreferenceService,
|
||||
getJimakuMaxEntryResultsService,
|
||||
getOverlayWindowsRuntimeService,
|
||||
handleCliCommandService,
|
||||
handleMineSentenceDigitService,
|
||||
handleMpvCommandFromIpcService,
|
||||
@@ -154,7 +128,6 @@ import {
|
||||
hasMpvWebsocketPlugin,
|
||||
initializeOverlayRuntimeService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
isGlobalShortcutRegisteredSafe,
|
||||
jimakuFetchJsonService,
|
||||
loadSubtitlePositionService,
|
||||
loadYomitanExtensionService,
|
||||
@@ -211,7 +184,41 @@ if (process.platform === "linux") {
|
||||
}
|
||||
|
||||
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
||||
const CONFIG_DIR = path.join(os.homedir(), ".config", "SubMiner");
|
||||
function resolveConfigDir(): string {
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
|
||||
const baseDirs = Array.from(
|
||||
new Set([
|
||||
xdgConfigHome || path.join(os.homedir(), ".config"),
|
||||
path.join(os.homedir(), ".config"),
|
||||
]),
|
||||
);
|
||||
const appNames = ["SubMiner", "subminer"];
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const appName of appNames) {
|
||||
const dir = path.join(baseDir, appName);
|
||||
if (
|
||||
fs.existsSync(path.join(dir, "config.jsonc")) ||
|
||||
fs.existsSync(path.join(dir, "config.json"))
|
||||
) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const baseDir of baseDirs) {
|
||||
for (const appName of appNames) {
|
||||
const dir = path.join(baseDir, appName);
|
||||
if (fs.existsSync(dir)) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(baseDirs[0], "SubMiner");
|
||||
}
|
||||
|
||||
const CONFIG_DIR = resolveConfigDir();
|
||||
const USER_DATA_PATH = CONFIG_DIR;
|
||||
const configService = new ConfigService(CONFIG_DIR);
|
||||
const isDev =
|
||||
@@ -239,8 +246,6 @@ process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let invisibleWindow: BrowserWindow | null = null;
|
||||
let yomitanExt: Extension | null = null;
|
||||
let yomitanSettingsWindow: BrowserWindow | null = null;
|
||||
let yomitanParserWindow: BrowserWindow | null = null;
|
||||
@@ -250,8 +255,6 @@ let mpvClient: MpvIpcClient | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let currentSubText = "";
|
||||
let currentSubAssText = "";
|
||||
let visibleOverlayVisible = false;
|
||||
let invisibleOverlayVisible = false;
|
||||
let windowTracker: BaseWindowTracker | null = null;
|
||||
let subtitlePosition: SubtitlePosition | null = null;
|
||||
let currentMediaPath: string | null = null;
|
||||
@@ -292,12 +295,13 @@ let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
|
||||
let runtimeOptionsManager: RuntimeOptionsManager | null = null;
|
||||
let trackerNotReadyWarningShown = false;
|
||||
let overlayDebugVisualizationEnabled = false;
|
||||
const overlayManager = createOverlayManagerService();
|
||||
type OverlayHostedModal = "runtime-options" | "subsync";
|
||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({
|
||||
getMainWindow: () => mainWindow,
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||
getResolver: () => fieldGroupingResolver,
|
||||
@@ -315,7 +319,7 @@ const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, "subtitle-positions");
|
||||
function getRuntimeOptionsState(): RuntimeOptionState[] { if (!runtimeOptionsManager) return []; return runtimeOptionsManager.listOptions(); }
|
||||
|
||||
function getOverlayWindows(): BrowserWindow[] {
|
||||
return getOverlayWindowsRuntimeService({ mainWindow, invisibleWindow });
|
||||
return overlayManager.getOverlayWindows();
|
||||
}
|
||||
|
||||
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
||||
@@ -492,13 +496,14 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
createMpvClient: () => {
|
||||
mpvClient = new MpvIpcClient(
|
||||
mpvSocketPath,
|
||||
createMpvIpcClientDepsRuntimeService({
|
||||
{
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
autoStartOverlay,
|
||||
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
isVisibleOverlayVisible: () =>
|
||||
overlayManager.getVisibleOverlayVisible(),
|
||||
getReconnectTimer: () => reconnectTimer,
|
||||
setReconnectTimer: (timer) => {
|
||||
reconnectTimer = timer;
|
||||
@@ -532,11 +537,12 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
showMpvOsd: (text) => {
|
||||
showMpvOsd(text);
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
},
|
||||
reloadConfig: () => {
|
||||
configService.reloadConfig();
|
||||
appLogger.logInfo(`Using config file: ${configService.getConfigPath()}`);
|
||||
},
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
@@ -705,7 +711,7 @@ function handleCliCommand(
|
||||
},
|
||||
app: {
|
||||
stop: () => app.quit(),
|
||||
hasMainWindow: () => Boolean(mainWindow),
|
||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||
},
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn, delayMs) => setTimeout(fn, delayMs),
|
||||
@@ -773,10 +779,10 @@ function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
||||
|
||||
function enforceOverlayLayerOrder(): void {
|
||||
enforceOverlayLayerOrderService({
|
||||
visibleOverlayVisible,
|
||||
invisibleOverlayVisible,
|
||||
mainWindow,
|
||||
invisibleWindow,
|
||||
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
|
||||
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
|
||||
mainWindow: overlayManager.getMainWindow(),
|
||||
invisibleWindow: overlayManager.getInvisibleWindow(),
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||
});
|
||||
}
|
||||
@@ -810,23 +816,31 @@ function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
|
||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||
setOverlayDebugVisualizationEnabled: (enabled) =>
|
||||
setOverlayDebugVisualizationEnabled(enabled),
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||
tryHandleOverlayShortcutLocalFallback(input),
|
||||
onWindowClosed: (windowKind) => {
|
||||
if (windowKind === "visible") {
|
||||
mainWindow = null;
|
||||
overlayManager.setMainWindow(null);
|
||||
} else {
|
||||
invisibleWindow = null;
|
||||
overlayManager.setInvisibleWindow(null);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function createMainWindow(): BrowserWindow { mainWindow = createOverlayWindow("visible"); return mainWindow; }
|
||||
function createInvisibleWindow(): BrowserWindow { invisibleWindow = createOverlayWindow("invisible"); return invisibleWindow; }
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const window = createOverlayWindow("visible");
|
||||
overlayManager.setMainWindow(window);
|
||||
return window;
|
||||
}
|
||||
function createInvisibleWindow(): BrowserWindow {
|
||||
const window = createOverlayWindow("invisible");
|
||||
overlayManager.setInvisibleWindow(window);
|
||||
return window;
|
||||
}
|
||||
|
||||
function initializeOverlayRuntime(): void {
|
||||
if (overlayRuntimeInitialized) {
|
||||
@@ -849,8 +863,9 @@ function initializeOverlayRuntime(): void {
|
||||
updateOverlayBounds: (geometry) => {
|
||||
updateOverlayBounds(geometry);
|
||||
},
|
||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
isInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
isInvisibleOverlayVisible: () =>
|
||||
overlayManager.getInvisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () => {
|
||||
updateVisibleOverlayVisibility();
|
||||
},
|
||||
@@ -875,35 +890,12 @@ function initializeOverlayRuntime(): void {
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
}),
|
||||
);
|
||||
invisibleOverlayVisible = result.invisibleOverlayVisible;
|
||||
overlayManager.setInvisibleOverlayVisible(result.invisibleOverlayVisible);
|
||||
overlayRuntimeInitialized = true;
|
||||
}
|
||||
|
||||
function getShortcutUiRuntimeDeps() {
|
||||
return {
|
||||
yomitanExt,
|
||||
getYomitanSettingsWindow: () => yomitanSettingsWindow,
|
||||
setYomitanSettingsWindow: (window: BrowserWindow | null) => {
|
||||
yomitanSettingsWindow = window;
|
||||
},
|
||||
shortcuts: getConfiguredShortcuts(),
|
||||
onToggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||
onToggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
||||
onOpenYomitanSettings: () => openYomitanSettings(),
|
||||
isDev,
|
||||
getMainWindow: () => mainWindow,
|
||||
getSecondarySubMode: () => secondarySubMode,
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
secondarySubMode = mode;
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => lastSecondarySubToggleAtMs,
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||
lastSecondarySubToggleAtMs = timestampMs;
|
||||
},
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
broadcastToOverlayWindows("secondary-subtitle:mode", mode);
|
||||
},
|
||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getOverlayShortcutFallbackHandlers: () =>
|
||||
getOverlayShortcutRuntimeHandlers().fallbackHandlers,
|
||||
@@ -913,12 +905,25 @@ function getShortcutUiRuntimeDeps() {
|
||||
|
||||
function openYomitanSettings(): void {
|
||||
openYomitanSettingsWindow(
|
||||
createYomitanSettingsWindowDepsRuntimeService(getShortcutUiRuntimeDeps()),
|
||||
{
|
||||
yomitanExt,
|
||||
getExistingWindow: () => yomitanSettingsWindow,
|
||||
setWindow: (window: BrowserWindow | null) => {
|
||||
yomitanSettingsWindow = window;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
function registerGlobalShortcuts(): void {
|
||||
registerGlobalShortcutsService(
|
||||
createGlobalShortcutRegistrationDepsRuntimeService(getShortcutUiRuntimeDeps()),
|
||||
{
|
||||
shortcuts: getConfiguredShortcuts(),
|
||||
onToggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||
onToggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
||||
onOpenYomitanSettings: () => openYomitanSettings(),
|
||||
isDev,
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -926,7 +931,7 @@ function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolve
|
||||
|
||||
function getOverlayShortcutRuntimeHandlers() {
|
||||
return createOverlayShortcutRuntimeHandlers(
|
||||
createOverlayShortcutRuntimeDepsService({
|
||||
{
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
openRuntimeOptions: () => {
|
||||
openRuntimeOptionsPalette();
|
||||
@@ -949,7 +954,7 @@ function getOverlayShortcutRuntimeHandlers() {
|
||||
mineSentenceMultiple: (timeoutMs) => {
|
||||
startPendingMineSentenceMultiple(timeoutMs);
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -962,7 +967,20 @@ function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
||||
|
||||
function cycleSecondarySubMode(): void {
|
||||
cycleSecondarySubModeService(
|
||||
createSecondarySubtitleCycleDepsRuntimeService(getShortcutUiRuntimeDeps()),
|
||||
{
|
||||
getSecondarySubMode: () => secondarySubMode,
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
secondarySubMode = mode;
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => lastSecondarySubToggleAtMs,
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||
lastSecondarySubToggleAtMs = timestampMs;
|
||||
},
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
broadcastToOverlayWindows("secondary-subtitle:mode", mode);
|
||||
},
|
||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -984,27 +1002,26 @@ const numericShortcutRuntime = createNumericShortcutRuntimeService({
|
||||
});
|
||||
const multiCopySession = numericShortcutRuntime.createSession();
|
||||
const mineSentenceSession = numericShortcutRuntime.createSession();
|
||||
const overlayVisibilityFacadeDeps =
|
||||
createOverlayVisibilityFacadeDepsRuntimeService({
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
setVisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
visibleOverlayVisible = nextVisible;
|
||||
},
|
||||
setInvisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
invisibleOverlayVisible = nextVisible;
|
||||
},
|
||||
updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
syncInvisibleOverlayMousePassthrough(),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||
setMpvSubVisibility: (mpvSubVisible: boolean) => {
|
||||
setMpvSubVisibilityRuntimeService(mpvClient, mpvSubVisible);
|
||||
},
|
||||
});
|
||||
const overlayVisibilityFacadeDeps = {
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
overlayManager.setVisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
setInvisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
overlayManager.setInvisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
syncInvisibleOverlayMousePassthrough(),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||
setMpvSubVisibility: (mpvSubVisible: boolean) => {
|
||||
setMpvSubVisibilityRuntimeService(mpvClient, mpvSubVisible);
|
||||
},
|
||||
};
|
||||
|
||||
function getSubsyncRuntimeDeps() {
|
||||
return createSubsyncRuntimeDepsService({
|
||||
@@ -1043,59 +1060,59 @@ function startPendingMultiCopy(timeoutMs: number): void {
|
||||
function handleMultiCopyDigit(count: number): void {
|
||||
handleMultiCopyDigitService(
|
||||
count,
|
||||
createHandleMultiCopyDigitDepsRuntimeService({
|
||||
{
|
||||
subtitleTimingTracker,
|
||||
writeClipboardText: (text) => clipboard.writeText(text),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function copyCurrentSubtitle(): void {
|
||||
copyCurrentSubtitleService(
|
||||
createCopyCurrentSubtitleDepsRuntimeService({
|
||||
{
|
||||
subtitleTimingTracker,
|
||||
writeClipboardText: (text) => clipboard.writeText(text),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function updateLastCardFromClipboard(): Promise<void> {
|
||||
await updateLastCardFromClipboardService(
|
||||
createUpdateLastCardFromClipboardDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
readClipboardText: () => clipboard.readText(),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function triggerFieldGrouping(): Promise<void> {
|
||||
await triggerFieldGroupingService(
|
||||
createTriggerFieldGroupingDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function markLastCardAsAudioCard(): Promise<void> {
|
||||
await markLastCardAsAudioCardService(
|
||||
createMarkLastCardAsAudioCardDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function mineSentenceCard(): Promise<void> {
|
||||
await mineSentenceCardService(
|
||||
createMineSentenceCardDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
mpvClient,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1118,7 +1135,7 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void {
|
||||
function handleMineSentenceDigit(count: number): void {
|
||||
handleMineSentenceDigitService(
|
||||
count,
|
||||
createHandleMineSentenceDigitDepsRuntimeService({
|
||||
{
|
||||
subtitleTimingTracker,
|
||||
ankiIntegration,
|
||||
getCurrentSecondarySubText: () =>
|
||||
@@ -1127,7 +1144,7 @@ function handleMineSentenceDigit(count: number): void {
|
||||
logError: (message, err) => {
|
||||
console.error(message, err);
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1139,12 +1156,12 @@ function registerOverlayShortcuts(): void {
|
||||
}
|
||||
|
||||
function getOverlayShortcutLifecycleDeps() {
|
||||
return createOverlayShortcutLifecycleDepsRuntimeService({
|
||||
return {
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers,
|
||||
cancelPendingMultiCopy: () => cancelPendingMultiCopy(),
|
||||
cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function unregisterOverlayShortcuts(): void {
|
||||
@@ -1173,8 +1190,8 @@ function refreshOverlayShortcuts(): void {
|
||||
function updateVisibleOverlayVisibility(): void {
|
||||
updateVisibleOverlayVisibilityService(
|
||||
createVisibleOverlayVisibilityDepsRuntimeService({
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getMainWindow: () => mainWindow,
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getWindowTracker: () => windowTracker,
|
||||
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
|
||||
setTrackerNotReadyWarningShown: (shown) => {
|
||||
@@ -1203,9 +1220,10 @@ function updateVisibleOverlayVisibility(): void {
|
||||
function updateInvisibleOverlayVisibility(): void {
|
||||
updateInvisibleOverlayVisibilityService(
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService({
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () =>
|
||||
overlayManager.getInvisibleOverlayVisible(),
|
||||
getWindowTracker: () => windowTracker,
|
||||
updateOverlayBounds: (geometry) => updateOverlayBounds(geometry),
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||
@@ -1217,13 +1235,17 @@ function updateInvisibleOverlayVisibility(): void {
|
||||
|
||||
function syncInvisibleOverlayMousePassthrough(): void {
|
||||
syncInvisibleOverlayMousePassthroughService({
|
||||
hasInvisibleWindow: () => Boolean(invisibleWindow && !invisibleWindow.isDestroyed()),
|
||||
hasInvisibleWindow: () => {
|
||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
||||
return Boolean(invisibleWindow && !invisibleWindow.isDestroyed());
|
||||
},
|
||||
setIgnoreMouseEvents: (ignore, extra) => {
|
||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
||||
invisibleWindow.setIgnoreMouseEvents(ignore, extra);
|
||||
},
|
||||
visibleOverlayVisible,
|
||||
invisibleOverlayVisible,
|
||||
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
|
||||
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1285,10 +1307,11 @@ const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDepsRuntimeService({
|
||||
|
||||
registerIpcHandlersService(
|
||||
createIpcDepsRuntimeService({
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
getMainWindow: () => mainWindow,
|
||||
getVisibleOverlayVisibility: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisibility: () => invisibleOverlayVisible,
|
||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisibility: () =>
|
||||
overlayManager.getInvisibleOverlayVisible(),
|
||||
onOverlayModalClosed: (modal) =>
|
||||
handleOverlayModalClosed(modal as OverlayHostedModal),
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
@@ -1316,7 +1339,7 @@ registerIpcHandlersService(
|
||||
);
|
||||
|
||||
registerAnkiJimakuIpcRuntimeService(
|
||||
createAnkiJimakuIpcDepsRuntimeService({
|
||||
{
|
||||
patchAnkiConnectEnabled: (enabled) => {
|
||||
configService.patchRawConfig({ ankiConnect: { enabled } });
|
||||
},
|
||||
@@ -1344,5 +1367,5 @@ registerAnkiJimakuIpcRuntimeService(
|
||||
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
||||
downloadToFile: (url, destPath, headers) =>
|
||||
downloadToFile(url, destPath, headers),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user