refactor: extract main runtime helper groups

- Extract remaining runtime helper clusters from main.ts into dedicated modules for readability:\n  - src/main/jlpt-runtime.ts\n  - src/main/media-runtime.ts\n  - src/main/overlay-visibility-runtime.ts\n- Wire main.ts to use the new runtime services and remove duplicated in-file helpers.\n- Preserve existing behavior via full typecheck + test:fast verification.\n- Finalize and archive TASK-56 backlog entry; update TASK-54 with completion metadata and summary.
This commit is contained in:
2026-02-15 21:18:20 -08:00
parent bec69d1b71
commit dae1f817e0
6 changed files with 372 additions and 193 deletions

77
src/main/jlpt-runtime.ts Normal file
View File

@@ -0,0 +1,77 @@
import * as path from "path";
import type { JlptLevel } from "../types";
import { createJlptVocabularyLookupService } from "../core/services";
export interface JlptDictionarySearchPathDeps {
getDictionaryRoots: () => string[];
}
export type JlptLookup = (term: string) => JlptLevel | null;
export interface JlptDictionaryRuntimeDeps {
isJlptEnabled: () => boolean;
getSearchPaths: () => string[];
setJlptLevelLookup: (lookup: JlptLookup) => void;
log: (message: string) => void;
}
let jlptDictionaryLookupInitialized = false;
let jlptDictionaryLookupInitialization: Promise<void> | null = null;
export function getJlptDictionarySearchPaths(
deps: JlptDictionarySearchPathDeps,
): string[] {
const dictionaryRoots = deps.getDictionaryRoots();
const searchPaths: string[] = [];
for (const dictionaryRoot of dictionaryRoots) {
searchPaths.push(dictionaryRoot);
searchPaths.push(path.join(dictionaryRoot, "vendor", "yomitan-jlpt-vocab"));
searchPaths.push(path.join(dictionaryRoot, "yomitan-jlpt-vocab"));
}
const uniquePaths = new Set<string>(searchPaths);
return [...uniquePaths];
}
export async function initializeJlptDictionaryLookup(
deps: JlptDictionaryRuntimeDeps,
): Promise<void> {
deps.setJlptLevelLookup(
await createJlptVocabularyLookupService({
searchPaths: deps.getSearchPaths(),
log: deps.log,
}),
);
}
export async function ensureJlptDictionaryLookup(
deps: JlptDictionaryRuntimeDeps,
): Promise<void> {
if (!deps.isJlptEnabled()) {
return;
}
if (jlptDictionaryLookupInitialized) {
return;
}
if (!jlptDictionaryLookupInitialization) {
jlptDictionaryLookupInitialization = initializeJlptDictionaryLookup(deps)
.then(() => {
jlptDictionaryLookupInitialized = true;
})
.catch((error) => {
jlptDictionaryLookupInitialization = null;
throw error;
});
}
await jlptDictionaryLookupInitialization;
}
export function createJlptDictionaryRuntimeService(
deps: JlptDictionaryRuntimeDeps,
): { ensureJlptDictionaryLookup: () => Promise<void> } {
return {
ensureJlptDictionaryLookup: () => ensureJlptDictionaryLookup(deps),
};
}

70
src/main/media-runtime.ts Normal file
View File

@@ -0,0 +1,70 @@
import { updateCurrentMediaPathService } from "../core/services";
import type { SubtitlePosition } from "../types";
export interface MediaRuntimeDeps {
isRemoteMediaPath: (mediaPath: string) => boolean;
loadSubtitlePosition: () => SubtitlePosition | null;
getCurrentMediaPath: () => string | null;
getPendingSubtitlePosition: () => SubtitlePosition | null;
getSubtitlePositionsDir: () => string;
setCurrentMediaPath: (mediaPath: string | null) => void;
clearPendingSubtitlePosition: () => void;
setSubtitlePosition: (position: SubtitlePosition | null) => void;
broadcastSubtitlePosition: (position: SubtitlePosition | null) => void;
getCurrentMediaTitle: () => string | null;
setCurrentMediaTitle: (title: string | null) => void;
}
export interface MediaRuntimeService {
updateCurrentMediaPath: (mediaPath: unknown) => void;
updateCurrentMediaTitle: (mediaTitle: unknown) => void;
resolveMediaPathForJimaku: (mediaPath: string | null) => string | null;
}
export function createMediaRuntimeService(
deps: MediaRuntimeDeps,
): MediaRuntimeService {
return {
updateCurrentMediaPath(mediaPath: unknown): void {
if (typeof mediaPath !== "string" || !deps.isRemoteMediaPath(mediaPath)) {
deps.setCurrentMediaTitle(null);
}
updateCurrentMediaPathService({
mediaPath,
currentMediaPath: deps.getCurrentMediaPath(),
pendingSubtitlePosition: deps.getPendingSubtitlePosition(),
subtitlePositionsDir: deps.getSubtitlePositionsDir(),
loadSubtitlePosition: () => deps.loadSubtitlePosition(),
setCurrentMediaPath: (nextPath: string | null) => {
deps.setCurrentMediaPath(nextPath);
},
clearPendingSubtitlePosition: () => {
deps.clearPendingSubtitlePosition();
},
setSubtitlePosition: (position: SubtitlePosition | null) => {
deps.setSubtitlePosition(position);
},
broadcastSubtitlePosition: (position: SubtitlePosition | null) => {
deps.broadcastSubtitlePosition(position);
},
});
},
updateCurrentMediaTitle(mediaTitle: unknown): void {
if (typeof mediaTitle === "string") {
const sanitized = mediaTitle.trim();
deps.setCurrentMediaTitle(sanitized.length > 0 ? sanitized : null);
return;
}
deps.setCurrentMediaTitle(null);
},
resolveMediaPathForJimaku(mediaPath: string | null): string | null {
return mediaPath && deps.isRemoteMediaPath(mediaPath) && deps.getCurrentMediaTitle()
? deps.getCurrentMediaTitle()
: mediaPath;
},
};
}

View File

@@ -0,0 +1,92 @@
import type { BrowserWindow } from "electron";
import type { BaseWindowTracker } from "../window-trackers";
import type { WindowGeometry } from "../types";
import {
syncInvisibleOverlayMousePassthroughService,
updateInvisibleOverlayVisibilityService,
updateVisibleOverlayVisibilityService,
} from "../core/services";
export interface OverlayVisibilityRuntimeDeps {
getMainWindow: () => BrowserWindow | null;
getInvisibleWindow: () => BrowserWindow | null;
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
getWindowTracker: () => BaseWindowTracker | null;
getTrackerNotReadyWarningShown: () => boolean;
setTrackerNotReadyWarningShown: (shown: boolean) => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
enforceOverlayLayerOrder: () => void;
syncOverlayShortcuts: () => void;
}
export interface OverlayVisibilityRuntimeService {
updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
}
export function createOverlayVisibilityRuntimeService(
deps: OverlayVisibilityRuntimeDeps,
): OverlayVisibilityRuntimeService {
const hasInvisibleWindow = (): boolean => {
const invisibleWindow = deps.getInvisibleWindow();
return Boolean(invisibleWindow && !invisibleWindow.isDestroyed());
};
const setIgnoreMouseEvents = (
ignore: boolean,
options?: Parameters<BrowserWindow["setIgnoreMouseEvents"]>[1],
): void => {
const invisibleWindow = deps.getInvisibleWindow();
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
invisibleWindow.setIgnoreMouseEvents(ignore, options);
};
return {
updateVisibleOverlayVisibility(): void {
updateVisibleOverlayVisibilityService({
visibleOverlayVisible: deps.getVisibleOverlayVisible(),
mainWindow: deps.getMainWindow(),
windowTracker: deps.getWindowTracker(),
trackerNotReadyWarningShown: deps.getTrackerNotReadyWarningShown(),
setTrackerNotReadyWarningShown: (shown: boolean) => {
deps.setTrackerNotReadyWarningShown(shown);
},
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
deps.updateVisibleOverlayBounds(geometry),
ensureOverlayWindowLevel: (window: BrowserWindow) =>
deps.ensureOverlayWindowLevel(window),
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
});
},
updateInvisibleOverlayVisibility(): void {
updateInvisibleOverlayVisibilityService({
invisibleWindow: deps.getInvisibleWindow(),
visibleOverlayVisible: deps.getVisibleOverlayVisible(),
invisibleOverlayVisible: deps.getInvisibleOverlayVisible(),
windowTracker: deps.getWindowTracker(),
updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
deps.updateInvisibleOverlayBounds(geometry),
ensureOverlayWindowLevel: (window: BrowserWindow) =>
deps.ensureOverlayWindowLevel(window),
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
});
},
syncInvisibleOverlayMousePassthrough(): void {
syncInvisibleOverlayMousePassthroughService({
hasInvisibleWindow,
setIgnoreMouseEvents,
visibleOverlayVisible: deps.getVisibleOverlayVisible(),
invisibleOverlayVisible: deps.getInvisibleOverlayVisible(),
});
},
};
}