mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor state and overlay runtime helpers
This commit is contained in:
15
src/main/cli-runtime.ts
Normal file
15
src/main/cli-runtime.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { handleCliCommandService, createCliCommandDepsRuntimeService } from "../core/services";
|
||||
import type { CliArgs, CliCommandSource } from "../cli/args";
|
||||
import { createCliCommandRuntimeServiceDeps, CliCommandRuntimeServiceDepsParams } from "./dependencies";
|
||||
|
||||
export function handleCliCommandRuntimeService(
|
||||
args: CliArgs,
|
||||
source: CliCommandSource,
|
||||
params: CliCommandRuntimeServiceDepsParams,
|
||||
): void {
|
||||
const deps = createCliCommandDepsRuntimeService(
|
||||
createCliCommandRuntimeServiceDeps(params),
|
||||
);
|
||||
handleCliCommandService(args, source, deps);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,19 @@ import {
|
||||
AnkiJimakuIpcRuntimeServiceDepsParams,
|
||||
createMainIpcRuntimeServiceDeps,
|
||||
MainIpcRuntimeServiceDepsParams,
|
||||
createRuntimeOptionsIpcDeps,
|
||||
RuntimeOptionsIpcDepsParams,
|
||||
} from "./dependencies";
|
||||
|
||||
export interface RegisterIpcRuntimeServicesParams {
|
||||
runtimeOptions: RuntimeOptionsIpcDepsParams;
|
||||
mainDeps: Omit<
|
||||
MainIpcRuntimeServiceDepsParams,
|
||||
"setRuntimeOption" | "cycleRuntimeOption"
|
||||
>;
|
||||
ankiJimakuDeps: AnkiJimakuIpcRuntimeServiceDepsParams;
|
||||
}
|
||||
|
||||
export function registerMainIpcRuntimeServices(
|
||||
params: MainIpcRuntimeServiceDepsParams,
|
||||
): void {
|
||||
@@ -26,3 +37,17 @@ export function registerAnkiJimakuIpcRuntimeServices(
|
||||
);
|
||||
}
|
||||
|
||||
export function registerIpcRuntimeServices(
|
||||
params: RegisterIpcRuntimeServicesParams,
|
||||
): void {
|
||||
const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDeps({
|
||||
getRuntimeOptionsManager: params.runtimeOptions.getRuntimeOptionsManager,
|
||||
showMpvOsd: params.runtimeOptions.showMpvOsd,
|
||||
});
|
||||
registerMainIpcRuntimeServices({
|
||||
...params.mainDeps,
|
||||
setRuntimeOption: runtimeOptionsIpcDeps.setRuntimeOption,
|
||||
cycleRuntimeOption: runtimeOptionsIpcDeps.cycleRuntimeOption,
|
||||
});
|
||||
registerAnkiJimakuIpcRuntimeServices(params.ankiJimakuDeps);
|
||||
}
|
||||
|
||||
140
src/main/overlay-runtime.ts
Normal file
140
src/main/overlay-runtime.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { BrowserWindow } from "electron";
|
||||
|
||||
type OverlayHostedModal = "runtime-options" | "subsync" | "jimaku";
|
||||
type OverlayHostLayer = "visible" | "invisible";
|
||||
|
||||
export interface OverlayWindowResolver {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
}
|
||||
|
||||
export interface OverlayModalRuntime {
|
||||
sendToActiveOverlayWindow: (
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||
) => boolean;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
handleOverlayModalClosed: (modal: OverlayHostedModal) => void;
|
||||
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
||||
}
|
||||
|
||||
export function createOverlayModalRuntimeService(
|
||||
deps: OverlayWindowResolver,
|
||||
): OverlayModalRuntime {
|
||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||
const overlayModalAutoShownLayer = new Map<OverlayHostedModal, OverlayHostLayer>();
|
||||
|
||||
const getTargetOverlayWindow = (): {
|
||||
window: BrowserWindow;
|
||||
layer: OverlayHostLayer;
|
||||
} | null => {
|
||||
const visibleMainWindow = deps.getMainWindow();
|
||||
const invisibleWindow = deps.getInvisibleWindow();
|
||||
|
||||
if (visibleMainWindow && !visibleMainWindow.isDestroyed()) {
|
||||
return { window: visibleMainWindow, layer: "visible" };
|
||||
}
|
||||
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
return { window: invisibleWindow, layer: "invisible" };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const showOverlayWindowForModal = (window: BrowserWindow, layer: OverlayHostLayer): void => {
|
||||
if (layer === "invisible" && typeof window.showInactive === "function") {
|
||||
window.showInactive();
|
||||
} else {
|
||||
window.show();
|
||||
}
|
||||
if (!window.isFocused()) {
|
||||
window.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const sendToActiveOverlayWindow = (
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||
): boolean => {
|
||||
const target = getTargetOverlayWindow();
|
||||
if (!target) return false;
|
||||
|
||||
const { window: targetWindow, layer } = target;
|
||||
const wasVisible = targetWindow.isVisible();
|
||||
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
||||
|
||||
const sendNow = (): void => {
|
||||
if (payload === undefined) {
|
||||
targetWindow.webContents.send(channel);
|
||||
} else {
|
||||
targetWindow.webContents.send(channel, payload);
|
||||
}
|
||||
};
|
||||
|
||||
if (!wasVisible) {
|
||||
showOverlayWindowForModal(targetWindow, layer);
|
||||
}
|
||||
if (!wasVisible && restoreOnModalClose) {
|
||||
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
||||
overlayModalAutoShownLayer.set(restoreOnModalClose, layer);
|
||||
}
|
||||
|
||||
if (targetWindow.webContents.isLoading()) {
|
||||
targetWindow.webContents.once("did-finish-load", () => {
|
||||
if (
|
||||
targetWindow &&
|
||||
!targetWindow.isDestroyed() &&
|
||||
!targetWindow.webContents.isLoading()
|
||||
) {
|
||||
sendNow();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
sendNow();
|
||||
return true;
|
||||
};
|
||||
|
||||
const openRuntimeOptionsPalette = (): void => {
|
||||
sendToActiveOverlayWindow("runtime-options:open", undefined, {
|
||||
restoreOnModalClose: "runtime-options",
|
||||
});
|
||||
};
|
||||
|
||||
const handleOverlayModalClosed = (modal: OverlayHostedModal): void => {
|
||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||
const layer = overlayModalAutoShownLayer.get(modal);
|
||||
overlayModalAutoShownLayer.delete(modal);
|
||||
if (!layer) return;
|
||||
const shouldKeepLayerVisible = [...restoreVisibleOverlayOnModalClose].some(
|
||||
(pendingModal) => overlayModalAutoShownLayer.get(pendingModal) === layer,
|
||||
);
|
||||
if (shouldKeepLayerVisible) return;
|
||||
|
||||
if (layer === "visible") {
|
||||
const mainWindow = deps.getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const invisibleWindow = deps.getInvisibleWindow();
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
invisibleWindow.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
sendToActiveOverlayWindow,
|
||||
openRuntimeOptionsPalette,
|
||||
handleOverlayModalClosed,
|
||||
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
||||
};
|
||||
}
|
||||
|
||||
export type { OverlayHostedModal };
|
||||
128
src/main/state.ts
Normal file
128
src/main/state.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { BrowserWindow, Extension } from "electron";
|
||||
|
||||
import type {
|
||||
Keybinding,
|
||||
MpvSubtitleRenderMetrics,
|
||||
SecondarySubMode,
|
||||
SubtitlePosition,
|
||||
KikuFieldGroupingChoice,
|
||||
} from "../types";
|
||||
import type { CliArgs } from "../cli/args";
|
||||
import type { SubtitleTimingTracker } from "../subtitle-timing-tracker";
|
||||
import type { AnkiIntegration } from "../anki-integration";
|
||||
import type { MpvIpcClient } from "../core/services";
|
||||
import { DEFAULT_MPV_SUBTITLE_RENDER_METRICS } from "../core/services";
|
||||
import type { RuntimeOptionsManager } from "../runtime-options";
|
||||
import type { MecabTokenizer } from "../mecab-tokenizer";
|
||||
import type { BaseWindowTracker } from "../window-trackers";
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface AppStateInitialValues {
|
||||
mpvSocketPath: string;
|
||||
texthookerPort: number;
|
||||
backendOverride?: string | null;
|
||||
autoStartOverlay?: boolean;
|
||||
texthookerOnlyMode?: boolean;
|
||||
}
|
||||
|
||||
export interface StartupState {
|
||||
initialArgs: Exclude<AppState["initialArgs"], null>;
|
||||
mpvSocketPath: AppState["mpvSocketPath"];
|
||||
texthookerPort: AppState["texthookerPort"];
|
||||
backendOverride: AppState["backendOverride"];
|
||||
autoStartOverlay: AppState["autoStartOverlay"];
|
||||
texthookerOnlyMode: AppState["texthookerOnlyMode"];
|
||||
}
|
||||
|
||||
export function createAppState(values: AppStateInitialValues): AppState {
|
||||
return {
|
||||
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,
|
||||
},
|
||||
runtimeOptionsManager: null,
|
||||
trackerNotReadyWarningShown: false,
|
||||
overlayDebugVisualizationEnabled: false,
|
||||
shortcutsRegistered: false,
|
||||
overlayRuntimeInitialized: false,
|
||||
fieldGroupingResolver: null,
|
||||
fieldGroupingResolverSequence: 0,
|
||||
subsyncInProgress: false,
|
||||
initialArgs: null,
|
||||
mpvSocketPath: values.mpvSocketPath,
|
||||
texthookerPort: values.texthookerPort,
|
||||
backendOverride: values.backendOverride ?? null,
|
||||
autoStartOverlay: values.autoStartOverlay ?? false,
|
||||
texthookerOnlyMode: values.texthookerOnlyMode ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyStartupState(
|
||||
appState: AppState,
|
||||
startupState: StartupState,
|
||||
): void {
|
||||
appState.initialArgs = startupState.initialArgs;
|
||||
appState.mpvSocketPath = startupState.mpvSocketPath;
|
||||
appState.texthookerPort = startupState.texthookerPort;
|
||||
appState.backendOverride = startupState.backendOverride;
|
||||
appState.autoStartOverlay = startupState.autoStartOverlay;
|
||||
appState.texthookerOnlyMode = startupState.texthookerOnlyMode;
|
||||
}
|
||||
43
src/main/subsync-runtime.ts
Normal file
43
src/main/subsync-runtime.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { SubsyncResolvedConfig } from "../subsync/utils";
|
||||
import type { SubsyncManualPayload, SubsyncManualRunRequest, SubsyncResult } from "../types";
|
||||
import type { SubsyncRuntimeDeps } from "../core/services/subsync-runner-service";
|
||||
import { createSubsyncRuntimeDeps } from "./dependencies";
|
||||
import { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "../core/services";
|
||||
|
||||
export interface SubsyncRuntimeServiceInput {
|
||||
getMpvClient: SubsyncRuntimeDeps["getMpvClient"];
|
||||
getResolvedSubsyncConfig: () => SubsyncResolvedConfig;
|
||||
isSubsyncInProgress: SubsyncRuntimeDeps["isSubsyncInProgress"];
|
||||
setSubsyncInProgress: SubsyncRuntimeDeps["setSubsyncInProgress"];
|
||||
showMpvOsd: SubsyncRuntimeDeps["showMpvOsd"];
|
||||
openManualPicker: (payload: SubsyncManualPayload) => void;
|
||||
}
|
||||
|
||||
export function createSubsyncRuntimeServiceDeps(
|
||||
params: SubsyncRuntimeServiceInput,
|
||||
): SubsyncRuntimeDeps {
|
||||
return createSubsyncRuntimeDeps({
|
||||
getMpvClient: params.getMpvClient,
|
||||
getResolvedSubsyncConfig: params.getResolvedSubsyncConfig,
|
||||
isSubsyncInProgress: params.isSubsyncInProgress,
|
||||
setSubsyncInProgress: params.setSubsyncInProgress,
|
||||
showMpvOsd: params.showMpvOsd,
|
||||
openManualPicker: params.openManualPicker,
|
||||
});
|
||||
}
|
||||
|
||||
export function triggerSubsyncFromConfigRuntime(
|
||||
params: SubsyncRuntimeServiceInput,
|
||||
): Promise<void> {
|
||||
return triggerSubsyncFromConfigRuntimeService(createSubsyncRuntimeServiceDeps(params));
|
||||
}
|
||||
|
||||
export async function runSubsyncManualFromIpcRuntime(
|
||||
request: SubsyncManualRunRequest,
|
||||
params: SubsyncRuntimeServiceInput,
|
||||
): Promise<SubsyncResult> {
|
||||
return runSubsyncManualFromIpcRuntimeService(
|
||||
request,
|
||||
createSubsyncRuntimeServiceDeps(params),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user