import * as path from 'path'; import type { AnilistCharacterDictionaryProfileScope, FrequencyDictionaryLookup, KikuFieldGroupingChoice, KikuFieldGroupingRequestData, SubtitlePosition, ResolvedConfig, } from '../types'; import { createBuildDictionaryRootsMainHandler, createBuildFrequencyDictionaryRuntimeMainDepsHandler, createBuildJlptDictionaryRuntimeMainDepsHandler, } from './runtime/dictionary-runtime-main-deps'; import { createImmersionMediaRuntime } from './runtime/immersion-media'; import { createFrequencyDictionaryRuntimeService, getFrequencyDictionarySearchPaths, } from './frequency-dictionary-runtime'; import { createJlptDictionaryRuntimeService, getJlptDictionarySearchPaths, type JlptLookup, } from './jlpt-runtime'; import { createMediaRuntimeService } from './media-runtime'; import { createCharacterDictionaryRuntimeService, type CharacterDictionaryBuildResult, } from './character-dictionary-runtime'; import type { AnilistMediaGuess } from '../core/services/anilist/anilist-updater'; import { createCharacterDictionaryAutoSyncRuntimeService, type CharacterDictionaryAutoSyncConfig, type CharacterDictionaryAutoSyncStatusEvent, } from './runtime/character-dictionary-auto-sync'; import { handleCharacterDictionaryAutoSyncComplete } from './runtime/character-dictionary-auto-sync-completion'; import { notifyCharacterDictionaryAutoSyncStatus } from './runtime/character-dictionary-auto-sync-notifications'; import { createFieldGroupingOverlayRuntime } from '../core/services/field-grouping-overlay'; type FieldGroupingResolver = ((choice: KikuFieldGroupingChoice) => void) | null; type BrowserWindowLike = { isDestroyed: () => boolean; webContents: { send: (channel: string, payload?: unknown) => void; }; }; type ImmersionTrackerLike = { handleMediaChange: (path: string, title: string | null) => void; }; type MpvClientLike = { currentVideoPath?: string | null; connected?: boolean; requestProperty?: (name: string) => Promise; }; type CharacterDictionaryAutoSyncCompleteDeps = { hasParserWindow: () => boolean; clearParserCaches: () => void; invalidateTokenizationCache: () => void; refreshSubtitlePrefetch: () => void; refreshCurrentSubtitle: () => void; logInfo: (message: string) => void; }; export interface DictionarySupportRuntimeInput { platform: NodeJS.Platform; dirname: string; appPath: string; resourcesPath: string; userDataPath: string; appUserDataPath: string; homeDir: string; appDataDir?: string; cwd: string; subtitlePositionsDir: string; getResolvedConfig: () => ResolvedConfig; isJlptEnabled: () => boolean; isFrequencyDictionaryEnabled: () => boolean; getFrequencyDictionarySourcePath: () => string | undefined; setJlptLevelLookup: (lookup: JlptLookup) => void; setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) => void; logInfo: (message: string) => void; logDebug?: (message: string) => void; logWarn: (message: string) => void; isRemoteMediaPath: (mediaPath: string) => boolean; getCurrentMediaPath: () => string | null; setCurrentMediaPath: (mediaPath: string | null) => void; getCurrentMediaTitle: () => string | null; setCurrentMediaTitle: (title: string | null) => void; getPendingSubtitlePosition: () => SubtitlePosition | null; loadSubtitlePosition: () => SubtitlePosition | null; clearPendingSubtitlePosition: () => void; setSubtitlePosition: (position: SubtitlePosition | null) => void; broadcastSubtitlePosition: (position: SubtitlePosition | null) => void; broadcastToOverlayWindows: (channel: string, payload?: unknown) => void; getTracker: () => ImmersionTrackerLike | null; getMpvClient: () => MpvClientLike | null; defaultImmersionDbPath: string; guessAnilistMediaInfo: ( mediaPath: string | null, mediaTitle: string | null, ) => Promise; getCollapsibleSectionOpenState: ( section: keyof ResolvedConfig['anilist']['characterDictionary']['collapsibleSections'], ) => boolean; isCharacterDictionaryEnabled: () => boolean; isYoutubePlaybackActiveNow: () => boolean; waitForYomitanMutationReady: () => Promise; getYomitanDictionaryInfo: () => Promise>; importYomitanDictionary: (zipPath: string) => Promise; deleteYomitanDictionary: (dictionaryTitle: string) => Promise; upsertYomitanDictionarySettings: ( dictionaryTitle: string, profileScope: AnilistCharacterDictionaryProfileScope, ) => Promise; getCharacterDictionaryConfig: () => CharacterDictionaryAutoSyncConfig; notifyCharacterDictionaryAutoSyncStatus: (event: CharacterDictionaryAutoSyncStatusEvent) => void; characterDictionaryAutoSyncCompleteDeps: CharacterDictionaryAutoSyncCompleteDeps; getMainWindow: () => BrowserWindowLike | null; getVisibleOverlayVisible: () => boolean; setVisibleOverlayVisible: (visible: boolean) => void; getRestoreVisibleOverlayOnModalClose: () => Set; sendToActiveOverlayWindow: ( channel: string, payload?: unknown, runtimeOptions?: { restoreOnModalClose?: TModal }, ) => boolean; } export interface DictionarySupportRuntime { ensureJlptDictionaryLookup: () => Promise; ensureFrequencyDictionaryLookup: () => Promise; getFieldGroupingResolver: () => FieldGroupingResolver; setFieldGroupingResolver: (resolver: FieldGroupingResolver) => void; createFieldGroupingCallback: () => ( data: KikuFieldGroupingRequestData, ) => Promise; getConfiguredDbPath: () => string; seedImmersionMediaFromCurrentMedia: () => Promise; syncImmersionMediaState: () => void; resolveMediaPathForJimaku: (mediaPath: string | null) => string | null; updateCurrentMediaPath: (mediaPath: unknown) => void; updateCurrentMediaTitle: (mediaTitle: unknown) => void; scheduleCharacterDictionarySync: () => void; generateCharacterDictionaryForCurrentMedia: ( targetPath?: string, ) => Promise; } export function createDictionarySupportRuntime( input: DictionarySupportRuntimeInput, ): DictionarySupportRuntime { const dictionaryRoots = createBuildDictionaryRootsMainHandler({ platform: input.platform, dirname: input.dirname, appPath: input.appPath, resourcesPath: input.resourcesPath, userDataPath: input.userDataPath, appUserDataPath: input.appUserDataPath, homeDir: input.homeDir, appDataDir: input.appDataDir, cwd: input.cwd, joinPath: (...parts: string[]) => path.join(...parts), }); const jlptRuntime = createJlptDictionaryRuntimeService( createBuildJlptDictionaryRuntimeMainDepsHandler({ isJlptEnabled: () => input.isJlptEnabled(), getDictionaryRoots: () => dictionaryRoots(), getJlptDictionarySearchPaths, setJlptLevelLookup: (lookup) => input.setJlptLevelLookup(lookup), logInfo: (message) => input.logInfo(message), })(), ); const frequencyRuntime = createFrequencyDictionaryRuntimeService( createBuildFrequencyDictionaryRuntimeMainDepsHandler({ isFrequencyDictionaryEnabled: () => input.isFrequencyDictionaryEnabled(), getDictionaryRoots: () => dictionaryRoots(), getFrequencyDictionarySearchPaths, getSourcePath: () => input.getFrequencyDictionarySourcePath(), setFrequencyRankLookup: (lookup) => input.setFrequencyRankLookup(lookup), logInfo: (message) => input.logInfo(message), })(), ); let fieldGroupingResolver: FieldGroupingResolver = null; let fieldGroupingResolverSequence = 0; const getFieldGroupingResolver = (): FieldGroupingResolver => fieldGroupingResolver; const setFieldGroupingResolver = (resolver: FieldGroupingResolver): void => { if (!resolver) { fieldGroupingResolver = null; return; } const sequence = ++fieldGroupingResolverSequence; fieldGroupingResolver = (choice) => { if (sequence !== fieldGroupingResolverSequence) { return; } resolver(choice); }; }; const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime({ getMainWindow: () => input.getMainWindow(), getVisibleOverlayVisible: () => input.getVisibleOverlayVisible(), setVisibleOverlayVisible: (visible) => input.setVisibleOverlayVisible(visible), getResolver: () => getFieldGroupingResolver(), setResolver: (resolver) => setFieldGroupingResolver(resolver), getRestoreVisibleOverlayOnModalClose: () => input.getRestoreVisibleOverlayOnModalClose(), sendToVisibleOverlay: (channel, payload, runtimeOptions) => input.sendToActiveOverlayWindow(channel, payload, runtimeOptions), }); const immersionMediaRuntime = createImmersionMediaRuntime({ getResolvedConfig: () => input.getResolvedConfig(), defaultImmersionDbPath: input.defaultImmersionDbPath, getTracker: () => input.getTracker(), getMpvClient: () => input.getMpvClient(), getCurrentMediaPath: () => input.getCurrentMediaPath(), getCurrentMediaTitle: () => input.getCurrentMediaTitle(), logDebug: (message) => (input.logDebug ?? input.logInfo)(message), logInfo: (message) => input.logInfo(message), }); const mediaRuntime = createMediaRuntimeService({ isRemoteMediaPath: (mediaPath) => input.isRemoteMediaPath(mediaPath), loadSubtitlePosition: () => input.loadSubtitlePosition(), getCurrentMediaPath: () => input.getCurrentMediaPath(), getPendingSubtitlePosition: () => input.getPendingSubtitlePosition(), getSubtitlePositionsDir: () => input.subtitlePositionsDir, setCurrentMediaPath: (mediaPath) => input.setCurrentMediaPath(mediaPath), clearPendingSubtitlePosition: () => input.clearPendingSubtitlePosition(), setSubtitlePosition: (position) => input.setSubtitlePosition(position), broadcastSubtitlePosition: (position) => input.broadcastSubtitlePosition(position), getCurrentMediaTitle: () => input.getCurrentMediaTitle(), setCurrentMediaTitle: (title) => input.setCurrentMediaTitle(title), }); const characterDictionaryRuntime = createCharacterDictionaryRuntimeService({ userDataPath: input.userDataPath, getCurrentMediaPath: () => input.getCurrentMediaPath(), getCurrentMediaTitle: () => input.getCurrentMediaTitle(), resolveMediaPathForJimaku: (mediaPath) => mediaRuntime.resolveMediaPathForJimaku(mediaPath), guessAnilistMediaInfo: (mediaPath, mediaTitle) => input.guessAnilistMediaInfo(mediaPath, mediaTitle), getCollapsibleSectionOpenState: (section) => input.getCollapsibleSectionOpenState(section), now: () => Date.now(), logInfo: (message) => input.logInfo(message), logWarn: (message) => input.logWarn(message), }); const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRuntimeService({ userDataPath: input.userDataPath, getConfig: () => input.getCharacterDictionaryConfig(), getOrCreateCurrentSnapshot: (targetPath, progress) => characterDictionaryRuntime.getOrCreateCurrentSnapshot(targetPath, progress), buildMergedDictionary: (mediaIds) => characterDictionaryRuntime.buildMergedDictionary(mediaIds), waitForYomitanMutationReady: () => input.waitForYomitanMutationReady(), getYomitanDictionaryInfo: () => input.getYomitanDictionaryInfo(), importYomitanDictionary: (zipPath) => input.importYomitanDictionary(zipPath), deleteYomitanDictionary: (dictionaryTitle) => input.deleteYomitanDictionary(dictionaryTitle), upsertYomitanDictionarySettings: (dictionaryTitle, profileScope) => input.upsertYomitanDictionarySettings(dictionaryTitle, profileScope), now: () => Date.now(), schedule: (fn, delayMs) => setTimeout(fn, delayMs), clearSchedule: (timer) => clearTimeout(timer), logInfo: (message) => input.logInfo(message), logWarn: (message) => input.logWarn(message), onSyncStatus: (event) => input.notifyCharacterDictionaryAutoSyncStatus(event), onSyncComplete: (result) => handleCharacterDictionaryAutoSyncComplete( result, input.characterDictionaryAutoSyncCompleteDeps, ), }); const scheduleCharacterDictionarySync = (): void => { if (!input.isCharacterDictionaryEnabled() || input.isYoutubePlaybackActiveNow()) { return; } characterDictionaryAutoSyncRuntime.scheduleSync(); }; return { ensureJlptDictionaryLookup: () => jlptRuntime.ensureJlptDictionaryLookup(), ensureFrequencyDictionaryLookup: () => frequencyRuntime.ensureFrequencyDictionaryLookup(), getFieldGroupingResolver, setFieldGroupingResolver, createFieldGroupingCallback: () => fieldGroupingOverlayRuntime.createFieldGroupingCallback(), getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(), seedImmersionMediaFromCurrentMedia: () => immersionMediaRuntime.seedFromCurrentMedia(), syncImmersionMediaState: () => immersionMediaRuntime.syncFromCurrentMediaState(), resolveMediaPathForJimaku: (mediaPath) => mediaRuntime.resolveMediaPathForJimaku(mediaPath), updateCurrentMediaPath: (mediaPath) => mediaRuntime.updateCurrentMediaPath(mediaPath), updateCurrentMediaTitle: (mediaTitle) => mediaRuntime.updateCurrentMediaTitle(mediaTitle), scheduleCharacterDictionarySync, generateCharacterDictionaryForCurrentMedia: (targetPath?: string) => characterDictionaryRuntime.generateForCurrentMedia(targetPath), }; }