refactor(mpv): emit media/path/title events for app-level handlers

This commit is contained in:
2026-02-14 01:13:21 -08:00
parent a1209ca69f
commit 656d686208
2 changed files with 378 additions and 225 deletions

View File

@@ -1,4 +1,5 @@
import * as net from "net"; import * as net from "net";
import { EventEmitter } from "events";
import { import {
Config, Config,
MpvClient, MpvClient,
@@ -44,7 +45,7 @@ interface SubtitleTimingTrackerLike {
recordSubtitle: (text: string, start: number, end: number) => void; recordSubtitle: (text: string, start: number, end: number) => void;
} }
export interface MpvIpcClientDeps { export interface MpvIpcClientProtocolDeps {
getResolvedConfig: () => Config; getResolvedConfig: () => Config;
autoStartOverlay: boolean; autoStartOverlay: boolean;
setOverlayVisible: (visible: boolean) => void; setOverlayVisible: (visible: boolean) => void;
@@ -52,29 +53,47 @@ export interface MpvIpcClientDeps {
isVisibleOverlayVisible: () => boolean; isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null; getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void; setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
getCurrentSubText: () => string; }
setCurrentSubText: (text: string) => void;
setCurrentSubAssText: (text: string) => void; export interface MpvIpcClientRuntimeDeps {
getSubtitleTimingTracker: () => SubtitleTimingTrackerLike | null; getCurrentSubText?: () => string;
subtitleWsBroadcast: (text: string) => void; setCurrentSubText?: (text: string) => void;
getOverlayWindowsCount: () => number; setCurrentSubAssText?: (text: string) => void;
tokenizeSubtitle: (text: string) => Promise<SubtitleData>; getSubtitleTimingTracker?: () => SubtitleTimingTrackerLike | null;
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void; subtitleWsBroadcast?: (text: string) => void;
updateCurrentMediaPath: (mediaPath: unknown) => void; getOverlayWindowsCount?: () => number;
updateMpvSubtitleRenderMetrics: ( tokenizeSubtitle?: (text: string) => Promise<SubtitleData>;
broadcastToOverlayWindows?: (channel: string, ...args: unknown[]) => void;
updateCurrentMediaPath?: (mediaPath: unknown) => void;
updateMpvSubtitleRenderMetrics?: (
patch: Partial<MpvSubtitleRenderMetrics>, patch: Partial<MpvSubtitleRenderMetrics>,
) => void; ) => void;
getMpvSubtitleRenderMetrics: () => MpvSubtitleRenderMetrics; getMpvSubtitleRenderMetrics?: () => MpvSubtitleRenderMetrics;
getPreviousSecondarySubVisibility: () => boolean | null; getPreviousSecondarySubVisibility?: () => boolean | null;
setPreviousSecondarySubVisibility: (value: boolean | null) => void; setPreviousSecondarySubVisibility?: (value: boolean | null) => void;
showMpvOsd: (text: string) => void; showMpvOsd?: (text: string) => void;
updateCurrentMediaTitle?: (mediaTitle: unknown) => void; updateCurrentMediaTitle?: (mediaTitle: unknown) => void;
} }
export interface MpvIpcClientDeps extends MpvIpcClientProtocolDeps, MpvIpcClientRuntimeDeps {}
export interface MpvIpcClientEventMap {
"subtitle-change": { text: string; isOverlayVisible: boolean };
"subtitle-ass-change": { text: string };
"secondary-subtitle-change": { text: string };
"media-path-change": { path: string };
"media-title-change": { title: string | null };
"subtitle-metrics-change": { patch: Partial<MpvSubtitleRenderMetrics> };
"secondary-subtitle-visibility": { visible: boolean };
}
type MpvIpcClientEventName = keyof MpvIpcClientEventMap;
export class MpvIpcClient implements MpvClient { export class MpvIpcClient implements MpvClient {
private socketPath: string; private socketPath: string;
private deps: MpvIpcClientDeps; private deps: MpvIpcClientProtocolDeps & Required<MpvIpcClientRuntimeDeps>;
public socket: net.Socket | null = null; public socket: net.Socket | null = null;
private eventBus = new EventEmitter();
private buffer = ""; private buffer = "";
public connected = false; public connected = false;
private connecting = false; private connecting = false;
@@ -96,7 +115,62 @@ export class MpvIpcClient implements MpvClient {
constructor(socketPath: string, deps: MpvIpcClientDeps) { constructor(socketPath: string, deps: MpvIpcClientDeps) {
this.socketPath = socketPath; this.socketPath = socketPath;
this.deps = deps; this.deps = {
getCurrentSubText: () => "",
setCurrentSubText: () => undefined,
setCurrentSubAssText: () => undefined,
getSubtitleTimingTracker: () => null,
subtitleWsBroadcast: () => undefined,
getOverlayWindowsCount: () => 0,
tokenizeSubtitle: async (text) => ({ text, tokens: null }),
broadcastToOverlayWindows: () => undefined,
updateCurrentMediaPath: () => undefined,
updateCurrentMediaTitle: () => undefined,
updateMpvSubtitleRenderMetrics: () => undefined,
getMpvSubtitleRenderMetrics: () => ({
subPos: 100,
subFontSize: 36,
subScale: 1,
subMarginY: 0,
subMarginX: 0,
subFont: "",
subSpacing: 0,
subBold: false,
subItalic: false,
subBorderSize: 0,
subShadowOffset: 0,
subAssOverride: "yes",
subScaleByWindow: true,
subUseMargins: true,
osdHeight: 0,
osdDimensions: null,
}),
getPreviousSecondarySubVisibility: () => null,
setPreviousSecondarySubVisibility: () => undefined,
showMpvOsd: () => undefined,
...deps,
};
}
on<EventName extends MpvIpcClientEventName>(
event: EventName,
listener: (payload: MpvIpcClientEventMap[EventName]) => void,
): void {
this.eventBus.on(event as string, listener);
}
off<EventName extends MpvIpcClientEventName>(
event: EventName,
listener: (payload: MpvIpcClientEventMap[EventName]) => void,
): void {
this.eventBus.off(event as string, listener);
}
private emit<EventName extends MpvIpcClientEventName>(
event: EventName,
payload: MpvIpcClientEventMap[EventName],
): void {
this.eventBus.emit(event as string, payload);
} }
setSocketPath(socketPath: string): void { setSocketPath(socketPath: string): void {
@@ -219,6 +293,11 @@ export class MpvIpcClient implements MpvClient {
if (msg.event === "property-change") { if (msg.event === "property-change") {
if (msg.name === "sub-text") { if (msg.name === "sub-text") {
const nextSubText = (msg.data as string) || ""; const nextSubText = (msg.data as string) || "";
const overlayVisible = this.deps.isVisibleOverlayVisible();
this.emit("subtitle-change", {
text: nextSubText,
isOverlayVisible: overlayVisible,
});
this.deps.setCurrentSubText(nextSubText); this.deps.setCurrentSubText(nextSubText);
this.currentSubText = nextSubText; this.currentSubText = nextSubText;
const subtitleTimingTracker = this.deps.getSubtitleTimingTracker(); const subtitleTimingTracker = this.deps.getSubtitleTimingTracker();
@@ -240,6 +319,9 @@ export class MpvIpcClient implements MpvClient {
} }
} else if (msg.name === "sub-text-ass") { } else if (msg.name === "sub-text-ass") {
const nextSubAssText = (msg.data as string) || ""; const nextSubAssText = (msg.data as string) || "";
this.emit("subtitle-ass-change", {
text: nextSubAssText,
});
this.deps.setCurrentSubAssText(nextSubAssText); this.deps.setCurrentSubAssText(nextSubAssText);
this.deps.broadcastToOverlayWindows("subtitle-ass:set", nextSubAssText); this.deps.broadcastToOverlayWindows("subtitle-ass:set", nextSubAssText);
} else if (msg.name === "sub-start") { } else if (msg.name === "sub-start") {
@@ -269,6 +351,9 @@ export class MpvIpcClient implements MpvClient {
} }
} else if (msg.name === "secondary-sub-text") { } else if (msg.name === "secondary-sub-text") {
this.currentSecondarySubText = (msg.data as string) || ""; this.currentSecondarySubText = (msg.data as string) || "";
this.emit("secondary-subtitle-change", {
text: this.currentSecondarySubText,
});
this.deps.broadcastToOverlayWindows( this.deps.broadcastToOverlayWindows(
"secondary-subtitle:set", "secondary-subtitle:set",
this.currentSecondarySubText, this.currentSecondarySubText,
@@ -287,13 +372,21 @@ export class MpvIpcClient implements MpvClient {
this.send({ command: ["set_property", "pause", true] }); this.send({ command: ["set_property", "pause", true] });
} }
} else if (msg.name === "media-title") { } else if (msg.name === "media-title") {
this.emit("media-title-change", {
title: typeof msg.data === "string" ? msg.data.trim() : null,
});
this.deps.updateCurrentMediaTitle?.(msg.data); this.deps.updateCurrentMediaTitle?.(msg.data);
} else if (msg.name === "path") { } else if (msg.name === "path") {
this.currentVideoPath = (msg.data as string) || ""; this.currentVideoPath = (msg.data as string) || "";
this.emit("media-path-change", {
path: (msg.data as string) || "",
});
this.deps.updateCurrentMediaPath(msg.data); this.deps.updateCurrentMediaPath(msg.data);
this.autoLoadSecondarySubTrack(); this.autoLoadSecondarySubTrack();
this.syncCurrentAudioStreamIndex(); this.syncCurrentAudioStreamIndex();
} else if (msg.name === "sub-pos") { } else if (msg.name === "sub-pos") {
const patch = { subPos: msg.data as number };
this.emit("subtitle-metrics-change", { patch });
this.deps.updateMpvSubtitleRenderMetrics({ subPos: msg.data as number }); this.deps.updateMpvSubtitleRenderMetrics({ subPos: msg.data as number });
} else if (msg.name === "sub-font-size") { } else if (msg.name === "sub-font-size") {
this.deps.updateMpvSubtitleRenderMetrics({ this.deps.updateMpvSubtitleRenderMetrics({
@@ -423,6 +516,10 @@ export class MpvIpcClient implements MpvClient {
const nextSubText = (msg.data as string) || ""; const nextSubText = (msg.data as string) || "";
this.deps.setCurrentSubText(nextSubText); this.deps.setCurrentSubText(nextSubText);
this.currentSubText = nextSubText; this.currentSubText = nextSubText;
this.emit("subtitle-change", {
text: nextSubText,
isOverlayVisible: this.deps.isVisibleOverlayVisible(),
});
this.deps.subtitleWsBroadcast(nextSubText); this.deps.subtitleWsBroadcast(nextSubText);
if (this.deps.getOverlayWindowsCount() > 0) { if (this.deps.getOverlayWindowsCount() > 0) {
this.deps.tokenizeSubtitle(nextSubText).then((subtitleData) => { this.deps.tokenizeSubtitle(nextSubText).then((subtitleData) => {
@@ -431,9 +528,15 @@ export class MpvIpcClient implements MpvClient {
} }
} else if (msg.request_id === MPV_REQUEST_ID_SUBTEXT_ASS) { } else if (msg.request_id === MPV_REQUEST_ID_SUBTEXT_ASS) {
const nextSubAssText = (msg.data as string) || ""; const nextSubAssText = (msg.data as string) || "";
this.emit("subtitle-ass-change", {
text: nextSubAssText,
});
this.deps.setCurrentSubAssText(nextSubAssText); this.deps.setCurrentSubAssText(nextSubAssText);
this.deps.broadcastToOverlayWindows("subtitle-ass:set", nextSubAssText); this.deps.broadcastToOverlayWindows("subtitle-ass:set", nextSubAssText);
} else if (msg.request_id === MPV_REQUEST_ID_PATH) { } else if (msg.request_id === MPV_REQUEST_ID_PATH) {
this.emit("media-path-change", {
path: (msg.data as string) || "",
});
this.deps.updateCurrentMediaPath(msg.data); this.deps.updateCurrentMediaPath(msg.data);
} else if (msg.request_id === MPV_REQUEST_ID_AID) { } else if (msg.request_id === MPV_REQUEST_ID_AID) {
this.currentAudioTrackId = this.currentAudioTrackId =

View File

@@ -265,39 +265,6 @@ process.on("SIGTERM", () => {
app.quit(); app.quit();
}); });
let yomitanExt: Extension | null = null;
let yomitanSettingsWindow: BrowserWindow | null = null;
let yomitanParserWindow: BrowserWindow | null = null;
let yomitanParserReadyPromise: Promise<void> | null = null;
let yomitanParserInitPromise: Promise<boolean> | null = null;
let mpvClient: MpvIpcClient | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let currentSubText = "";
let currentSubAssText = "";
let windowTracker: BaseWindowTracker | null = null;
let subtitlePosition: SubtitlePosition | null = null;
let currentMediaPath: string | null = null;
let currentMediaTitle: string | null = null;
let pendingSubtitlePosition: SubtitlePosition | null = null;
let mecabTokenizer: MecabTokenizer | null = null;
let keybindings: Keybinding[] = [];
let subtitleTimingTracker: SubtitleTimingTracker | null = null;
let ankiIntegration: AnkiIntegration | null = null;
let secondarySubMode: SecondarySubMode = "hover";
let lastSecondarySubToggleAtMs = 0;
let previousSecondarySubVisibility: boolean | null = null;
let mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics = {
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
};
let shortcutsRegistered = false;
let overlayRuntimeInitialized = false;
let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
null;
let fieldGroupingResolverSequence = 0;
let runtimeOptionsManager: RuntimeOptionsManager | null = null;
let trackerNotReadyWarningShown = false;
let overlayDebugVisualizationEnabled = false;
const overlayManager = createOverlayManagerService(); const overlayManager = createOverlayManagerService();
const overlayContentMeasurementStore = createOverlayContentMeasurementStoreService({ const overlayContentMeasurementStore = createOverlayContentMeasurementStoreService({
now: () => Date.now(), now: () => Date.now(),
@@ -310,23 +277,103 @@ type OverlayHostLayer = "visible" | "invisible";
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>(); const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>(); const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
interface AppState {
yomitanExt: Extension | null;
yomitanSettingsWindow: BrowserWindow | null;
yomitanParserWindow: BrowserWindow | null;
yomitanParserReadyPromise: Promise<void> | null;
yomitanParserInitPromise: Promise<boolean> | null;
mpvClient: MpvIpcClient | null;
reconnectTimer: ReturnType<typeof setTimeout> | null;
currentSubText: string;
currentSubAssText: string;
windowTracker: BaseWindowTracker | null;
subtitlePosition: SubtitlePosition | null;
currentMediaPath: string | null;
currentMediaTitle: string | null;
pendingSubtitlePosition: SubtitlePosition | null;
mecabTokenizer: MecabTokenizer | null;
keybindings: Keybinding[];
subtitleTimingTracker: SubtitleTimingTracker | null;
ankiIntegration: AnkiIntegration | null;
secondarySubMode: SecondarySubMode;
lastSecondarySubToggleAtMs: number;
previousSecondarySubVisibility: boolean | null;
mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics;
shortcutsRegistered: boolean;
overlayRuntimeInitialized: boolean;
fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null;
fieldGroupingResolverSequence: number;
runtimeOptionsManager: RuntimeOptionsManager | null;
trackerNotReadyWarningShown: boolean;
overlayDebugVisualizationEnabled: boolean;
subsyncInProgress: boolean;
initialArgs: CliArgs | null;
mpvSocketPath: string;
texthookerPort: number;
backendOverride: string | null;
autoStartOverlay: boolean;
texthookerOnlyMode: boolean;
}
const appState: AppState = {
yomitanExt: null,
yomitanSettingsWindow: null,
yomitanParserWindow: null,
yomitanParserReadyPromise: null,
yomitanParserInitPromise: null,
mpvClient: null,
reconnectTimer: null,
currentSubText: "",
currentSubAssText: "",
windowTracker: null,
subtitlePosition: null,
currentMediaPath: null,
currentMediaTitle: null,
pendingSubtitlePosition: null,
mecabTokenizer: null,
keybindings: [],
subtitleTimingTracker: null,
ankiIntegration: null,
secondarySubMode: "hover",
lastSecondarySubToggleAtMs: 0,
previousSecondarySubVisibility: null,
mpvSubtitleRenderMetrics: {
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
},
shortcutsRegistered: false,
overlayRuntimeInitialized: false,
fieldGroupingResolver: null,
fieldGroupingResolverSequence: 0,
runtimeOptionsManager: null,
trackerNotReadyWarningShown: false,
overlayDebugVisualizationEnabled: false,
subsyncInProgress: false,
initialArgs: null,
mpvSocketPath: getDefaultSocketPath(),
texthookerPort: DEFAULT_TEXTHOOKER_PORT,
backendOverride: null,
autoStartOverlay: false,
texthookerOnlyMode: false,
};
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null { function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
return fieldGroupingResolver; return appState.fieldGroupingResolver;
} }
function setFieldGroupingResolver( function setFieldGroupingResolver(
resolver: ((choice: KikuFieldGroupingChoice) => void) | null, resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
): void { ): void {
if (!resolver) { if (!resolver) {
fieldGroupingResolver = null; appState.fieldGroupingResolver = null;
return; return;
} }
const sequence = ++fieldGroupingResolverSequence; const sequence = ++appState.fieldGroupingResolverSequence;
const wrappedResolver = (choice: KikuFieldGroupingChoice): void => { const wrappedResolver = (choice: KikuFieldGroupingChoice): void => {
if (sequence !== fieldGroupingResolverSequence) return; if (sequence !== appState.fieldGroupingResolverSequence) return;
resolver(choice); resolver(choice);
}; };
fieldGroupingResolver = wrappedResolver; appState.fieldGroupingResolver = wrappedResolver;
} }
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({
@@ -348,15 +395,15 @@ const createFieldGroupingCallback =
const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, "subtitle-positions"); const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, "subtitle-positions");
function getRuntimeOptionsState(): RuntimeOptionState[] { if (!runtimeOptionsManager) return []; return runtimeOptionsManager.listOptions(); } function getRuntimeOptionsState(): RuntimeOptionState[] { if (!appState.runtimeOptionsManager) return []; return appState.runtimeOptionsManager.listOptions(); }
function getOverlayWindows(): BrowserWindow[] { function getOverlayWindows(): BrowserWindow[] {
return overlayManager.getOverlayWindows(); return overlayManager.getOverlayWindows();
} }
function restorePreviousSecondarySubVisibility(): void { function restorePreviousSecondarySubVisibility(): void {
if (!mpvClient || !mpvClient.connected) return; if (!appState.mpvClient || !appState.mpvClient.connected) return;
mpvClient.restorePreviousSecondarySubVisibility(); appState.mpvClient.restorePreviousSecondarySubVisibility();
} }
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void { function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
@@ -445,10 +492,10 @@ function sendToActiveOverlayWindow(
function setOverlayDebugVisualizationEnabled(enabled: boolean): void { function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
setOverlayDebugVisualizationEnabledRuntimeService( setOverlayDebugVisualizationEnabledRuntimeService(
overlayDebugVisualizationEnabled, appState.overlayDebugVisualizationEnabled,
enabled, enabled,
(next) => { (next) => {
overlayDebugVisualizationEnabled = next; appState.overlayDebugVisualizationEnabled = next;
}, },
(channel, ...args) => broadcastToOverlayWindows(channel, ...args), (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
); );
@@ -480,7 +527,7 @@ function shouldBindVisibleOverlayToMpvSubVisibility(): boolean {
function isAutoUpdateEnabledRuntime(): boolean { function isAutoUpdateEnabledRuntime(): boolean {
return isAutoUpdateEnabledRuntimeService( return isAutoUpdateEnabledRuntimeService(
getResolvedConfig(), getResolvedConfig(),
runtimeOptionsManager, appState.runtimeOptionsManager,
); );
} }
@@ -503,47 +550,47 @@ async function jimakuFetchJson<T>(
} }
function loadSubtitlePosition(): SubtitlePosition | null { function loadSubtitlePosition(): SubtitlePosition | null {
subtitlePosition = loadSubtitlePositionService({ appState.subtitlePosition = loadSubtitlePositionService({
currentMediaPath, currentMediaPath: appState.currentMediaPath,
fallbackPosition: getResolvedConfig().subtitlePosition, fallbackPosition: getResolvedConfig().subtitlePosition,
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR, subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
}); });
return subtitlePosition; return appState.subtitlePosition;
} }
function saveSubtitlePosition(position: SubtitlePosition): void { function saveSubtitlePosition(position: SubtitlePosition): void {
subtitlePosition = position; appState.subtitlePosition = position;
saveSubtitlePositionService({ saveSubtitlePositionService({
position, position,
currentMediaPath, currentMediaPath: appState.currentMediaPath,
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR, subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
onQueuePending: (queued) => { onQueuePending: (queued) => {
pendingSubtitlePosition = queued; appState.pendingSubtitlePosition = queued;
}, },
onPersisted: () => { onPersisted: () => {
pendingSubtitlePosition = null; appState.pendingSubtitlePosition = null;
}, },
}); });
} }
function updateCurrentMediaPath(mediaPath: unknown): void { function updateCurrentMediaPath(mediaPath: unknown): void {
if (typeof mediaPath !== "string" || !isRemoteMediaPath(mediaPath)) { if (typeof mediaPath !== "string" || !isRemoteMediaPath(mediaPath)) {
currentMediaTitle = null; appState.currentMediaTitle = null;
} }
updateCurrentMediaPathService({ updateCurrentMediaPathService({
mediaPath, mediaPath,
currentMediaPath, currentMediaPath: appState.currentMediaPath,
pendingSubtitlePosition, pendingSubtitlePosition: appState.pendingSubtitlePosition,
subtitlePositionsDir: SUBTITLE_POSITIONS_DIR, subtitlePositionsDir: SUBTITLE_POSITIONS_DIR,
loadSubtitlePosition: () => loadSubtitlePosition(), loadSubtitlePosition: () => loadSubtitlePosition(),
setCurrentMediaPath: (nextPath) => { setCurrentMediaPath: (nextPath) => {
currentMediaPath = nextPath; appState.currentMediaPath = nextPath;
}, },
clearPendingSubtitlePosition: () => { clearPendingSubtitlePosition: () => {
pendingSubtitlePosition = null; appState.pendingSubtitlePosition = null;
}, },
setSubtitlePosition: (position) => { setSubtitlePosition: (position) => {
subtitlePosition = position; appState.subtitlePosition = position;
}, },
broadcastSubtitlePosition: (position) => { broadcastSubtitlePosition: (position) => {
broadcastToOverlayWindows("subtitle-position:set", position); broadcastToOverlayWindows("subtitle-position:set", position);
@@ -554,26 +601,18 @@ function updateCurrentMediaPath(mediaPath: unknown): void {
function updateCurrentMediaTitle(mediaTitle: unknown): void { function updateCurrentMediaTitle(mediaTitle: unknown): void {
if (typeof mediaTitle === "string") { if (typeof mediaTitle === "string") {
const sanitized = mediaTitle.trim(); const sanitized = mediaTitle.trim();
currentMediaTitle = sanitized.length > 0 ? sanitized : null; appState.currentMediaTitle = sanitized.length > 0 ? sanitized : null;
return; return;
} }
currentMediaTitle = null; appState.currentMediaTitle = null;
} }
function resolveMediaPathForJimaku(mediaPath: string | null): string | null { function resolveMediaPathForJimaku(mediaPath: string | null): string | null {
return mediaPath && isRemoteMediaPath(mediaPath) && currentMediaTitle return mediaPath && isRemoteMediaPath(mediaPath) && appState.currentMediaTitle
? currentMediaTitle ? appState.currentMediaTitle
: mediaPath; : mediaPath;
} }
let subsyncInProgress = false;
let initialArgs: CliArgs;
let mpvSocketPath = getDefaultSocketPath();
let texthookerPort = DEFAULT_TEXTHOOKER_PORT;
let backendOverride: string | null = null;
let autoStartOverlay = false;
let texthookerOnlyMode = false;
const startupState = runStartupBootstrapRuntimeService({ const startupState = runStartupBootstrapRuntimeService({
argv: process.argv, argv: process.argv,
parseArgs: (argv) => parseArgs(argv), parseArgs: (argv) => parseArgs(argv),
@@ -624,31 +663,31 @@ const startupState = runStartupBootstrapRuntimeService({
await runAppReadyRuntimeService({ await runAppReadyRuntimeService({
loadSubtitlePosition: () => loadSubtitlePosition(), loadSubtitlePosition: () => loadSubtitlePosition(),
resolveKeybindings: () => { resolveKeybindings: () => {
keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS); appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
}, },
createMpvClient: () => { createMpvClient: () => {
mpvClient = new MpvIpcClient( appState.mpvClient = new MpvIpcClient(
mpvSocketPath, appState.mpvSocketPath,
{ {
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
autoStartOverlay, autoStartOverlay: appState.autoStartOverlay,
setOverlayVisible: (visible) => setOverlayVisible(visible), setOverlayVisible: (visible) => setOverlayVisible(visible),
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility: () =>
shouldBindVisibleOverlayToMpvSubVisibility(), shouldBindVisibleOverlayToMpvSubVisibility(),
isVisibleOverlayVisible: () => isVisibleOverlayVisible: () =>
overlayManager.getVisibleOverlayVisible(), overlayManager.getVisibleOverlayVisible(),
getReconnectTimer: () => reconnectTimer, getReconnectTimer: () => appState.reconnectTimer,
setReconnectTimer: (timer) => { setReconnectTimer: (timer) => {
reconnectTimer = timer; appState.reconnectTimer = timer;
}, },
getCurrentSubText: () => currentSubText, getCurrentSubText: () => appState.currentSubText,
setCurrentSubText: (text) => { setCurrentSubText: (text) => {
currentSubText = text; appState.currentSubText = text;
}, },
setCurrentSubAssText: (text) => { setCurrentSubAssText: (text) => {
currentSubAssText = text; appState.currentSubAssText = text;
}, },
getSubtitleTimingTracker: () => subtitleTimingTracker, getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
subtitleWsBroadcast: (text) => { subtitleWsBroadcast: (text) => {
subtitleWsService.broadcast(text); subtitleWsService.broadcast(text);
}, },
@@ -657,26 +696,21 @@ const startupState = runStartupBootstrapRuntimeService({
broadcastToOverlayWindows: (channel, ...channelArgs) => { broadcastToOverlayWindows: (channel, ...channelArgs) => {
broadcastToOverlayWindows(channel, ...channelArgs); broadcastToOverlayWindows(channel, ...channelArgs);
}, },
updateCurrentMediaPath: (mediaPath) => {
updateCurrentMediaPath(mediaPath);
},
updateCurrentMediaTitle: (mediaTitle) => {
updateCurrentMediaTitle(mediaTitle);
},
updateMpvSubtitleRenderMetrics: (patch) => { updateMpvSubtitleRenderMetrics: (patch) => {
updateMpvSubtitleRenderMetrics(patch); updateMpvSubtitleRenderMetrics(patch);
}, },
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
getPreviousSecondarySubVisibility: () => getPreviousSecondarySubVisibility: () =>
previousSecondarySubVisibility, appState.previousSecondarySubVisibility,
setPreviousSecondarySubVisibility: (value) => { setPreviousSecondarySubVisibility: (value) => {
previousSecondarySubVisibility = value; appState.previousSecondarySubVisibility = value;
}, },
showMpvOsd: (text) => { showMpvOsd: (text) => {
showMpvOsd(text); showMpvOsd(text);
}, },
}, },
); );
bindMpvClientEventHandlers(appState.mpvClient);
}, },
reloadConfig: () => { reloadConfig: () => {
configService.reloadConfig(); configService.reloadConfig();
@@ -686,12 +720,12 @@ const startupState = runStartupBootstrapRuntimeService({
getConfigWarnings: () => configService.getWarnings(), getConfigWarnings: () => configService.getWarnings(),
logConfigWarning: (warning) => appLogger.logConfigWarning(warning), logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
initRuntimeOptionsManager: () => { initRuntimeOptionsManager: () => {
runtimeOptionsManager = new RuntimeOptionsManager( appState.runtimeOptionsManager = new RuntimeOptionsManager(
() => configService.getConfig().ankiConnect, () => configService.getConfig().ankiConnect,
{ {
applyAnkiPatch: (patch) => { applyAnkiPatch: (patch) => {
if (ankiIntegration) { if (appState.ankiIntegration) {
ankiIntegration.applyRuntimeConfigPatch(patch); appState.ankiIntegration.applyRuntimeConfigPatch(patch);
} }
}, },
onOptionsChanged: () => { onOptionsChanged: () => {
@@ -702,28 +736,28 @@ const startupState = runStartupBootstrapRuntimeService({
); );
}, },
setSecondarySubMode: (mode) => { setSecondarySubMode: (mode) => {
secondarySubMode = mode; appState.secondarySubMode = mode;
}, },
defaultSecondarySubMode: "hover", defaultSecondarySubMode: "hover",
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port, defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(), hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
startSubtitleWebsocket: (port) => { startSubtitleWebsocket: (port) => {
subtitleWsService.start(port, () => currentSubText); subtitleWsService.start(port, () => appState.currentSubText);
}, },
log: (message) => appLogger.logInfo(message), log: (message) => appLogger.logInfo(message),
createMecabTokenizerAndCheck: async () => { createMecabTokenizerAndCheck: async () => {
const tokenizer = new MecabTokenizer(); const tokenizer = new MecabTokenizer();
mecabTokenizer = tokenizer; appState.mecabTokenizer = tokenizer;
await tokenizer.checkAvailability(); await tokenizer.checkAvailability();
}, },
createSubtitleTimingTracker: () => { createSubtitleTimingTracker: () => {
const tracker = new SubtitleTimingTracker(); const tracker = new SubtitleTimingTracker();
subtitleTimingTracker = tracker; appState.subtitleTimingTracker = tracker;
}, },
loadYomitanExtension: async () => { loadYomitanExtension: async () => {
await loadYomitanExtension(); await loadYomitanExtension();
}, },
texthookerOnlyMode, texthookerOnlyMode: appState.texthookerOnlyMode,
shouldAutoInitializeOverlayRuntimeFromConfig: () => shouldAutoInitializeOverlayRuntimeFromConfig: () =>
shouldAutoInitializeOverlayRuntimeFromConfig(), shouldAutoInitializeOverlayRuntimeFromConfig(),
initializeOverlayRuntime: () => initializeOverlayRuntime(), initializeOverlayRuntime: () => initializeOverlayRuntime(),
@@ -735,30 +769,30 @@ const startupState = runStartupBootstrapRuntimeService({
globalShortcut.unregisterAll(); globalShortcut.unregisterAll();
subtitleWsService.stop(); subtitleWsService.stop();
texthookerService.stop(); texthookerService.stop();
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) { if (appState.yomitanParserWindow && !appState.yomitanParserWindow.isDestroyed()) {
yomitanParserWindow.destroy(); appState.yomitanParserWindow.destroy();
} }
yomitanParserWindow = null; appState.yomitanParserWindow = null;
yomitanParserReadyPromise = null; appState.yomitanParserReadyPromise = null;
yomitanParserInitPromise = null; appState.yomitanParserInitPromise = null;
if (windowTracker) { if (appState.windowTracker) {
windowTracker.stop(); appState.windowTracker.stop();
} }
if (mpvClient && mpvClient.socket) { if (appState.mpvClient && appState.mpvClient.socket) {
mpvClient.socket.destroy(); appState.mpvClient.socket.destroy();
} }
if (reconnectTimer) { if (appState.reconnectTimer) {
clearTimeout(reconnectTimer); clearTimeout(appState.reconnectTimer);
} }
if (subtitleTimingTracker) { if (appState.subtitleTimingTracker) {
subtitleTimingTracker.destroy(); appState.subtitleTimingTracker.destroy();
} }
if (ankiIntegration) { if (appState.ankiIntegration) {
ankiIntegration.destroy(); appState.ankiIntegration.destroy();
} }
}, },
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivate: () =>
overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0, appState.overlayRuntimeInitialized && BrowserWindow.getAllWindows().length === 0,
restoreWindowsOnActivate: () => { restoreWindowsOnActivate: () => {
createMainWindow(); createMainWindow();
createInvisibleWindow(); createInvisibleWindow();
@@ -769,12 +803,12 @@ const startupState = runStartupBootstrapRuntimeService({
}, },
}); });
initialArgs = startupState.initialArgs; appState.initialArgs = startupState.initialArgs;
mpvSocketPath = startupState.mpvSocketPath; appState.mpvSocketPath = startupState.mpvSocketPath;
texthookerPort = startupState.texthookerPort; appState.texthookerPort = startupState.texthookerPort;
backendOverride = startupState.backendOverride; appState.backendOverride = startupState.backendOverride;
autoStartOverlay = startupState.autoStartOverlay; appState.autoStartOverlay = startupState.autoStartOverlay;
texthookerOnlyMode = startupState.texthookerOnlyMode; appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
function handleCliCommand( function handleCliCommand(
args: CliArgs, args: CliArgs,
@@ -782,18 +816,18 @@ function handleCliCommand(
): void { ): void {
const deps = createCliCommandDepsRuntimeService({ const deps = createCliCommandDepsRuntimeService({
mpv: { mpv: {
getSocketPath: () => mpvSocketPath, getSocketPath: () => appState.mpvSocketPath,
setSocketPath: (socketPath) => { setSocketPath: (socketPath) => {
mpvSocketPath = socketPath; appState.mpvSocketPath = socketPath;
}, },
getClient: () => mpvClient, getClient: () => appState.mpvClient,
showOsd: (text) => showMpvOsd(text), showOsd: (text) => showMpvOsd(text),
}, },
texthooker: { texthooker: {
service: texthookerService, service: texthookerService,
getPort: () => texthookerPort, getPort: () => appState.texthookerPort,
setPort: (port) => { setPort: (port) => {
texthookerPort = port; appState.texthookerPort = port;
}, },
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false, shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
openInBrowser: (url) => { openInBrowser: (url) => {
@@ -803,7 +837,7 @@ function handleCliCommand(
}, },
}, },
overlay: { overlay: {
isInitialized: () => overlayRuntimeInitialized, isInitialized: () => appState.overlayRuntimeInitialized,
initialize: () => initializeOverlayRuntime(), initialize: () => initializeOverlayRuntime(),
toggleVisible: () => toggleVisibleOverlay(), toggleVisible: () => toggleVisibleOverlay(),
toggleInvisible: () => toggleInvisibleOverlay(), toggleInvisible: () => toggleInvisibleOverlay(),
@@ -847,21 +881,37 @@ function handleCliCommand(
} }
function handleInitialArgs(): void { function handleInitialArgs(): void {
handleCliCommand(initialArgs, "initial"); if (!appState.initialArgs) return;
handleCliCommand(appState.initialArgs, "initial");
}
function bindMpvClientEventHandlers(mpvClient: MpvIpcClient): void {
mpvClient.on("media-path-change", ({ path }) => {
updateCurrentMediaPath(path);
});
mpvClient.on("media-title-change", ({ title }) => {
updateCurrentMediaTitle(title);
});
mpvClient.on("subtitle-ass-change", ({ text }) => {
appState.currentSubAssText = text;
});
mpvClient.on("subtitle-change", ({ text }) => {
appState.currentSubText = text;
});
} }
function updateMpvSubtitleRenderMetrics( function updateMpvSubtitleRenderMetrics(
patch: Partial<MpvSubtitleRenderMetrics>, patch: Partial<MpvSubtitleRenderMetrics>,
): void { ): void {
const { next, changed } = applyMpvSubtitleRenderMetricsPatchService( const { next, changed } = applyMpvSubtitleRenderMetricsPatchService(
mpvSubtitleRenderMetrics, appState.mpvSubtitleRenderMetrics,
patch, patch,
); );
if (!changed) return; if (!changed) return;
mpvSubtitleRenderMetrics = next; appState.mpvSubtitleRenderMetrics = next;
broadcastToOverlayWindows( broadcastToOverlayWindows(
"mpv-subtitle-render-metrics:set", "mpv-subtitle-render-metrics:set",
mpvSubtitleRenderMetrics, appState.mpvSubtitleRenderMetrics,
); );
} }
@@ -869,20 +919,20 @@ async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
return tokenizeSubtitleService( return tokenizeSubtitleService(
text, text,
createTokenizerDepsRuntimeService({ createTokenizerDepsRuntimeService({
getYomitanExt: () => yomitanExt, getYomitanExt: () => appState.yomitanExt,
getYomitanParserWindow: () => yomitanParserWindow, getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
yomitanParserWindow = window; appState.yomitanParserWindow = window;
}, },
getYomitanParserReadyPromise: () => yomitanParserReadyPromise, getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
setYomitanParserReadyPromise: (promise) => { setYomitanParserReadyPromise: (promise) => {
yomitanParserReadyPromise = promise; appState.yomitanParserReadyPromise = promise;
}, },
getYomitanParserInitPromise: () => yomitanParserInitPromise, getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
setYomitanParserInitPromise: (promise) => { setYomitanParserInitPromise: (promise) => {
yomitanParserInitPromise = promise; appState.yomitanParserInitPromise = promise;
}, },
getMecabTokenizer: () => mecabTokenizer, getMecabTokenizer: () => appState.mecabTokenizer,
}), }),
); );
} }
@@ -912,18 +962,18 @@ function enforceOverlayLayerOrder(): void {
async function loadYomitanExtension(): Promise<Extension | null> { async function loadYomitanExtension(): Promise<Extension | null> {
return loadYomitanExtensionService({ return loadYomitanExtensionService({
userDataPath: USER_DATA_PATH, userDataPath: USER_DATA_PATH,
getYomitanParserWindow: () => yomitanParserWindow, getYomitanParserWindow: () => appState.yomitanParserWindow,
setYomitanParserWindow: (window) => { setYomitanParserWindow: (window) => {
yomitanParserWindow = window; appState.yomitanParserWindow = window;
}, },
setYomitanParserReadyPromise: (promise) => { setYomitanParserReadyPromise: (promise) => {
yomitanParserReadyPromise = promise; appState.yomitanParserReadyPromise = promise;
}, },
setYomitanParserInitPromise: (promise) => { setYomitanParserInitPromise: (promise) => {
yomitanParserInitPromise = promise; appState.yomitanParserInitPromise = promise;
}, },
setYomitanExtension: (extension) => { setYomitanExtension: (extension) => {
yomitanExt = extension; appState.yomitanExt = extension;
}, },
}); });
} }
@@ -933,7 +983,7 @@ function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
kind, kind,
{ {
isDev, isDev,
overlayDebugVisualizationEnabled, overlayDebugVisualizationEnabled: appState.overlayDebugVisualizationEnabled,
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(), onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled: (enabled) =>
@@ -967,12 +1017,12 @@ function createInvisibleWindow(): BrowserWindow {
} }
function initializeOverlayRuntime(): void { function initializeOverlayRuntime(): void {
if (overlayRuntimeInitialized) { if (appState.overlayRuntimeInitialized) {
return; return;
} }
const result = initializeOverlayRuntimeService( const result = initializeOverlayRuntimeService(
{ {
backendOverride, backendOverride: appState.backendOverride,
getInitialInvisibleOverlayVisibility: () => getInitialInvisibleOverlayVisibility: () =>
getInitialInvisibleOverlayVisibility(), getInitialInvisibleOverlayVisibility(),
createMainWindow: () => { createMainWindow: () => {
@@ -1004,30 +1054,30 @@ function initializeOverlayRuntime(): void {
syncOverlayShortcuts(); syncOverlayShortcuts();
}, },
setWindowTracker: (tracker) => { setWindowTracker: (tracker) => {
windowTracker = tracker; appState.windowTracker = tracker;
}, },
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
getSubtitleTimingTracker: () => subtitleTimingTracker, getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
getMpvClient: () => mpvClient, getMpvClient: () => appState.mpvClient,
getRuntimeOptionsManager: () => runtimeOptionsManager, getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
setAnkiIntegration: (integration) => { setAnkiIntegration: (integration) => {
ankiIntegration = integration as AnkiIntegration | null; appState.ankiIntegration = integration as AnkiIntegration | null;
}, },
showDesktopNotification, showDesktopNotification,
createFieldGroupingCallback: () => createFieldGroupingCallback(), createFieldGroupingCallback: () => createFieldGroupingCallback(),
}, },
); );
overlayManager.setInvisibleOverlayVisible(result.invisibleOverlayVisible); overlayManager.setInvisibleOverlayVisible(result.invisibleOverlayVisible);
overlayRuntimeInitialized = true; appState.overlayRuntimeInitialized = true;
} }
function openYomitanSettings(): void { function openYomitanSettings(): void {
openYomitanSettingsWindow( openYomitanSettingsWindow(
{ {
yomitanExt, yomitanExt: appState.yomitanExt,
getExistingWindow: () => yomitanSettingsWindow, getExistingWindow: () => appState.yomitanSettingsWindow,
setWindow: (window: BrowserWindow | null) => { setWindow: (window: BrowserWindow | null) => {
yomitanSettingsWindow = window; appState.yomitanSettingsWindow = window;
}, },
}, },
); );
@@ -1090,13 +1140,13 @@ function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
function cycleSecondarySubMode(): void { function cycleSecondarySubMode(): void {
cycleSecondarySubModeService( cycleSecondarySubModeService(
{ {
getSecondarySubMode: () => secondarySubMode, getSecondarySubMode: () => appState.secondarySubMode,
setSecondarySubMode: (mode: SecondarySubMode) => { setSecondarySubMode: (mode: SecondarySubMode) => {
secondarySubMode = mode; appState.secondarySubMode = mode;
}, },
getLastSecondarySubToggleAtMs: () => lastSecondarySubToggleAtMs, getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
setLastSecondarySubToggleAtMs: (timestampMs: number) => { setLastSecondarySubToggleAtMs: (timestampMs: number) => {
lastSecondarySubToggleAtMs = timestampMs; appState.lastSecondarySubToggleAtMs = timestampMs;
}, },
broadcastSecondarySubMode: (mode: SecondarySubMode) => { broadcastSecondarySubMode: (mode: SecondarySubMode) => {
broadcastToOverlayWindows("secondary-subtitle:mode", mode); broadcastToOverlayWindows("secondary-subtitle:mode", mode);
@@ -1109,7 +1159,7 @@ function cycleSecondarySubMode(): void {
function showMpvOsd(text: string): void { function showMpvOsd(text: string): void {
appendToMpvLog(`[OSD] ${text}`); appendToMpvLog(`[OSD] ${text}`);
showMpvOsdRuntimeService( showMpvOsdRuntimeService(
mpvClient, appState.mpvClient,
text, text,
(line) => { (line) => {
console.log(line); console.log(line);
@@ -1141,11 +1191,11 @@ const mineSentenceSession = numericShortcutRuntime.createSession();
function getSubsyncRuntimeDeps() { function getSubsyncRuntimeDeps() {
return { return {
getMpvClient: () => mpvClient, getMpvClient: () => appState.mpvClient,
getResolvedSubsyncConfig: () => getSubsyncConfig(getResolvedConfig().subsync), getResolvedSubsyncConfig: () => getSubsyncConfig(getResolvedConfig().subsync),
isSubsyncInProgress: () => subsyncInProgress, isSubsyncInProgress: () => appState.subsyncInProgress,
setSubsyncInProgress: (inProgress: boolean) => { setSubsyncInProgress: (inProgress: boolean) => {
subsyncInProgress = inProgress; appState.subsyncInProgress = inProgress;
}, },
showMpvOsd: (text: string) => showMpvOsd(text), showMpvOsd: (text: string) => showMpvOsd(text),
openManualPicker: (payload: SubsyncManualPayload) => { openManualPicker: (payload: SubsyncManualPayload) => {
@@ -1180,7 +1230,7 @@ function handleMultiCopyDigit(count: number): void {
handleMultiCopyDigitService( handleMultiCopyDigitService(
count, count,
{ {
subtitleTimingTracker, subtitleTimingTracker: appState.subtitleTimingTracker,
writeClipboardText: (text) => clipboard.writeText(text), writeClipboardText: (text) => clipboard.writeText(text),
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}, },
@@ -1190,7 +1240,7 @@ function handleMultiCopyDigit(count: number): void {
function copyCurrentSubtitle(): void { function copyCurrentSubtitle(): void {
copyCurrentSubtitleService( copyCurrentSubtitleService(
{ {
subtitleTimingTracker, subtitleTimingTracker: appState.subtitleTimingTracker,
writeClipboardText: (text) => clipboard.writeText(text), writeClipboardText: (text) => clipboard.writeText(text),
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}, },
@@ -1200,7 +1250,7 @@ function copyCurrentSubtitle(): void {
async function updateLastCardFromClipboard(): Promise<void> { async function updateLastCardFromClipboard(): Promise<void> {
await updateLastCardFromClipboardService( await updateLastCardFromClipboardService(
{ {
ankiIntegration, ankiIntegration: appState.ankiIntegration,
readClipboardText: () => clipboard.readText(), readClipboardText: () => clipboard.readText(),
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}, },
@@ -1210,7 +1260,7 @@ async function updateLastCardFromClipboard(): Promise<void> {
async function triggerFieldGrouping(): Promise<void> { async function triggerFieldGrouping(): Promise<void> {
await triggerFieldGroupingService( await triggerFieldGroupingService(
{ {
ankiIntegration, ankiIntegration: appState.ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}, },
); );
@@ -1219,7 +1269,7 @@ async function triggerFieldGrouping(): Promise<void> {
async function markLastCardAsAudioCard(): Promise<void> { async function markLastCardAsAudioCard(): Promise<void> {
await markLastCardAsAudioCardService( await markLastCardAsAudioCardService(
{ {
ankiIntegration, ankiIntegration: appState.ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}, },
); );
@@ -1228,8 +1278,8 @@ async function markLastCardAsAudioCard(): Promise<void> {
async function mineSentenceCard(): Promise<void> { async function mineSentenceCard(): Promise<void> {
await mineSentenceCardService( await mineSentenceCardService(
{ {
ankiIntegration, ankiIntegration: appState.ankiIntegration,
mpvClient, mpvClient: appState.mpvClient,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}, },
); );
@@ -1255,10 +1305,10 @@ function handleMineSentenceDigit(count: number): void {
handleMineSentenceDigitService( handleMineSentenceDigitService(
count, count,
{ {
subtitleTimingTracker, subtitleTimingTracker: appState.subtitleTimingTracker,
ankiIntegration, ankiIntegration: appState.ankiIntegration,
getCurrentSecondarySubText: () => getCurrentSecondarySubText: () =>
mpvClient?.currentSecondarySubText || undefined, appState.mpvClient?.currentSecondarySubText || undefined,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
logError: (message, err) => { logError: (message, err) => {
console.error(message, err); console.error(message, err);
@@ -1268,7 +1318,7 @@ function handleMineSentenceDigit(count: number): void {
} }
function registerOverlayShortcuts(): void { function registerOverlayShortcuts(): void {
shortcutsRegistered = registerOverlayShortcutsService( appState.shortcutsRegistered = registerOverlayShortcutsService(
getConfiguredShortcuts(), getConfiguredShortcuts(),
getOverlayShortcutRuntimeHandlers().overlayHandlers, getOverlayShortcutRuntimeHandlers().overlayHandlers,
); );
@@ -1284,24 +1334,24 @@ function getOverlayShortcutLifecycleDeps() {
} }
function unregisterOverlayShortcuts(): void { function unregisterOverlayShortcuts(): void {
shortcutsRegistered = unregisterOverlayShortcutsRuntimeService( appState.shortcutsRegistered = unregisterOverlayShortcutsRuntimeService(
shortcutsRegistered, appState.shortcutsRegistered,
getOverlayShortcutLifecycleDeps(), getOverlayShortcutLifecycleDeps(),
); );
} }
function shouldOverlayShortcutsBeActive(): boolean { return overlayRuntimeInitialized; } function shouldOverlayShortcutsBeActive(): boolean { return appState.overlayRuntimeInitialized; }
function syncOverlayShortcuts(): void { function syncOverlayShortcuts(): void {
shortcutsRegistered = syncOverlayShortcutsRuntimeService( appState.shortcutsRegistered = syncOverlayShortcutsRuntimeService(
shouldOverlayShortcutsBeActive(), shouldOverlayShortcutsBeActive(),
shortcutsRegistered, appState.shortcutsRegistered,
getOverlayShortcutLifecycleDeps(), getOverlayShortcutLifecycleDeps(),
); );
} }
function refreshOverlayShortcuts(): void { function refreshOverlayShortcuts(): void {
shortcutsRegistered = refreshOverlayShortcutsRuntimeService( appState.shortcutsRegistered = refreshOverlayShortcutsRuntimeService(
shouldOverlayShortcutsBeActive(), shouldOverlayShortcutsBeActive(),
shortcutsRegistered, appState.shortcutsRegistered,
getOverlayShortcutLifecycleDeps(), getOverlayShortcutLifecycleDeps(),
); );
} }
@@ -1311,10 +1361,10 @@ function updateVisibleOverlayVisibility(): void {
{ {
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(), visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
mainWindow: overlayManager.getMainWindow(), mainWindow: overlayManager.getMainWindow(),
windowTracker, windowTracker: appState.windowTracker,
trackerNotReadyWarningShown, trackerNotReadyWarningShown: appState.trackerNotReadyWarningShown,
setTrackerNotReadyWarningShown: (shown) => { setTrackerNotReadyWarningShown: (shown) => {
trackerNotReadyWarningShown = shown; appState.trackerNotReadyWarningShown = shown;
}, },
updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry), updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry),
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
@@ -1330,7 +1380,7 @@ function updateInvisibleOverlayVisibility(): void {
invisibleWindow: overlayManager.getInvisibleWindow(), invisibleWindow: overlayManager.getInvisibleWindow(),
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(), visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(), invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
windowTracker, windowTracker: appState.windowTracker,
updateInvisibleOverlayBounds: (geometry) => updateInvisibleOverlayBounds(geometry), updateInvisibleOverlayBounds: (geometry) => updateInvisibleOverlayBounds(geometry),
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(), enforceOverlayLayerOrder: () => enforceOverlayLayerOrder(),
@@ -1367,9 +1417,9 @@ function setVisibleOverlayVisible(visible: boolean): void {
syncInvisibleOverlayMousePassthrough(), syncInvisibleOverlayMousePassthrough(),
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility: () =>
shouldBindVisibleOverlayToMpvSubVisibility(), shouldBindVisibleOverlayToMpvSubVisibility(),
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected), isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
setMpvSubVisibility: (mpvSubVisible) => { setMpvSubVisibility: (mpvSubVisible) => {
setMpvSubVisibilityRuntimeService(mpvClient, mpvSubVisible); setMpvSubVisibilityRuntimeService(appState.mpvClient, mpvSubVisible);
}, },
}); });
} }
@@ -1426,21 +1476,21 @@ function handleMpvCommandFromIpc(command: (string | number)[]): void {
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
runtimeOptionsCycle: (id, direction) => { runtimeOptionsCycle: (id, direction) => {
if (!runtimeOptionsManager) { if (!appState.runtimeOptionsManager) {
return { ok: false, error: "Runtime options manager unavailable" }; return { ok: false, error: "Runtime options manager unavailable" };
} }
return applyRuntimeOptionResultRuntimeService( return applyRuntimeOptionResultRuntimeService(
runtimeOptionsManager.cycleOption(id, direction), appState.runtimeOptionsManager.cycleOption(id, direction),
(text) => showMpvOsd(text), (text) => showMpvOsd(text),
); );
}, },
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(mpvClient), mpvReplaySubtitle: () => replayCurrentSubtitleRuntimeService(appState.mpvClient),
mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(mpvClient), mpvPlayNextSubtitle: () => playNextSubtitleRuntimeService(appState.mpvClient),
mpvSendCommand: (rawCommand) => mpvSendCommand: (rawCommand) =>
sendMpvCommandRuntimeService(mpvClient, rawCommand), sendMpvCommandRuntimeService(appState.mpvClient, rawCommand),
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected), isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
hasRuntimeOptionsManager: () => runtimeOptionsManager !== null, hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
}, },
); );
} }
@@ -1454,14 +1504,14 @@ async function runSubsyncManualFromIpc(
const runtimeOptionsIpcDeps = { const runtimeOptionsIpcDeps = {
setRuntimeOption: (id: string, value: unknown) => setRuntimeOption: (id: string, value: unknown) =>
setRuntimeOptionFromIpcRuntimeService( setRuntimeOptionFromIpcRuntimeService(
runtimeOptionsManager, appState.runtimeOptionsManager,
id as RuntimeOptionId, id as RuntimeOptionId,
value as RuntimeOptionValue, value as RuntimeOptionValue,
(text) => showMpvOsd(text), (text) => showMpvOsd(text),
), ),
cycleRuntimeOption: (id: string, direction: 1 | -1) => cycleRuntimeOption: (id: string, direction: 1 | -1) =>
cycleRuntimeOptionFromIpcRuntimeService( cycleRuntimeOptionFromIpcRuntimeService(
runtimeOptionsManager, appState.runtimeOptionsManager,
id as RuntimeOptionId, id as RuntimeOptionId,
direction, direction,
(text) => showMpvOsd(text), (text) => showMpvOsd(text),
@@ -1480,21 +1530,21 @@ registerIpcHandlersService(
openYomitanSettings: () => openYomitanSettings(), openYomitanSettings: () => openYomitanSettings(),
quitApp: () => app.quit(), quitApp: () => app.quit(),
toggleVisibleOverlay: () => toggleVisibleOverlay(), toggleVisibleOverlay: () => toggleVisibleOverlay(),
tokenizeCurrentSubtitle: () => tokenizeSubtitle(currentSubText), tokenizeCurrentSubtitle: () => tokenizeSubtitle(appState.currentSubText),
getCurrentSubtitleAss: () => currentSubAssText, getCurrentSubtitleAss: () => appState.currentSubAssText,
getMpvSubtitleRenderMetrics: () => mpvSubtitleRenderMetrics, getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
getSubtitlePosition: () => loadSubtitlePosition(), getSubtitlePosition: () => loadSubtitlePosition(),
getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null, getSubtitleStyle: () => getResolvedConfig().subtitleStyle ?? null,
saveSubtitlePosition: (position) => saveSubtitlePosition: (position) =>
saveSubtitlePosition(position as SubtitlePosition), saveSubtitlePosition(position as SubtitlePosition),
getMecabTokenizer: () => mecabTokenizer, getMecabTokenizer: () => appState.mecabTokenizer,
handleMpvCommand: (command) => handleMpvCommandFromIpc(command), handleMpvCommand: (command) => handleMpvCommandFromIpc(command),
getKeybindings: () => keybindings, getKeybindings: () => appState.keybindings,
getSecondarySubMode: () => secondarySubMode, getSecondarySubMode: () => appState.secondarySubMode,
getMpvClient: () => mpvClient, getMpvClient: () => appState.mpvClient,
runSubsyncManual: (request) => runSubsyncManual: (request) =>
runSubsyncManualFromIpc(request as SubsyncManualRunRequest), runSubsyncManualFromIpc(request as SubsyncManualRunRequest),
getAnkiConnectStatus: () => ankiIntegration !== null, getAnkiConnectStatus: () => appState.ankiIntegration !== null,
getRuntimeOptions: () => getRuntimeOptionsState(), getRuntimeOptions: () => getRuntimeOptionsState(),
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption, setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption, cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
@@ -1510,12 +1560,12 @@ registerAnkiJimakuIpcRuntimeService(
configService.patchRawConfig({ ankiConnect: { enabled } }); configService.patchRawConfig({ ankiConnect: { enabled } });
}, },
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
getRuntimeOptionsManager: () => runtimeOptionsManager, getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
getSubtitleTimingTracker: () => subtitleTimingTracker, getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
getMpvClient: () => mpvClient, getMpvClient: () => appState.mpvClient,
getAnkiIntegration: () => ankiIntegration, getAnkiIntegration: () => appState.ankiIntegration,
setAnkiIntegration: (integration) => { setAnkiIntegration: (integration) => {
ankiIntegration = integration; appState.ankiIntegration = integration;
}, },
showDesktopNotification, showDesktopNotification,
createFieldGroupingCallback: () => createFieldGroupingCallback(), createFieldGroupingCallback: () => createFieldGroupingCallback(),
@@ -1523,7 +1573,7 @@ registerAnkiJimakuIpcRuntimeService(
getFieldGroupingResolver: () => getFieldGroupingResolver(), getFieldGroupingResolver: () => getFieldGroupingResolver(),
setFieldGroupingResolver: (resolver) => setFieldGroupingResolver(resolver), setFieldGroupingResolver: (resolver) => setFieldGroupingResolver(resolver),
parseMediaInfo: (mediaPath) => parseMediaInfo(resolveMediaPathForJimaku(mediaPath)), parseMediaInfo: (mediaPath) => parseMediaInfo(resolveMediaPathForJimaku(mediaPath)),
getCurrentMediaPath: () => currentMediaPath, getCurrentMediaPath: () => appState.currentMediaPath,
jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query), jimakuFetchJson: (endpoint, query) => jimakuFetchJson(endpoint, query),
getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(), getJimakuMaxEntryResults: () => getJimakuMaxEntryResults(),
getJimakuLanguagePreference: () => getJimakuLanguagePreference(), getJimakuLanguagePreference: () => getJimakuLanguagePreference(),