mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
refactor(main): move IPC, CLI and app-ready wiring into assemblies (#311)
This commit is contained in:
1 parent
655510cf2e
commit
696462a82e
33 files changed
+3719
-2101
No files matched your search
@@ -0,0 +1,518 @@
|
||||
import path from 'node:path';
|
||||
import { AnkiIntegration } from '../../anki-integration';
|
||||
import { isHeadlessInitialCommand } from '../../cli/args';
|
||||
import { DEFAULT_CONFIG, DEFAULT_KEYBINDINGS, generateConfigTemplate } from '../../config';
|
||||
import type { ConfigService } from '../../config/service';
|
||||
import {
|
||||
initializeOverlayRuntime as initializeOverlayRuntimeCore,
|
||||
type createOverlayManager,
|
||||
type SubtitleWebSocket,
|
||||
type Texthooker,
|
||||
} from '../../core/services';
|
||||
import { resolveKeybindings, showDesktopNotification } from '../../core/utils';
|
||||
import type { Logger } from '../../logger';
|
||||
import { RuntimeOptionsManager } from '../../runtime-options';
|
||||
import { ensureDefaultConfigBootstrap, getDefaultConfigFilePaths } from '../../shared/setup-state';
|
||||
import { SubtitleTimingTracker } from '../../subtitle-timing-tracker';
|
||||
import type { SecondarySubMode, WindowGeometry } from '../../types';
|
||||
import { mergeAiConfig } from '../../ai/config';
|
||||
import type { OverlayModalRuntime } from '../overlay-runtime';
|
||||
import {
|
||||
composeAppReadyRuntime,
|
||||
type AppReadyComposerOptions,
|
||||
type composeOverlayVisibilityRuntime,
|
||||
} from '../runtime/composers';
|
||||
import type { createConfigDerivedRuntime } from '../runtime/domains/startup';
|
||||
import { shouldAutoOpenFirstRunSetup } from '../runtime/first-run-setup-service';
|
||||
import type { createOverlayGeometryRuntime } from '../runtime/overlay-geometry-runtime';
|
||||
import type { createOverlayNotificationsRuntime } from '../runtime/overlay-notifications-runtime';
|
||||
import { createOverlayRuntimeBootstrapHandlers } from '../runtime/overlay-runtime-bootstrap-handlers';
|
||||
import {
|
||||
getStartupModeFlags,
|
||||
shouldHandleInitialArgsBeforeDeferredOverlayWarmup,
|
||||
shouldRefreshAnilistOnConfigReload,
|
||||
} from '../runtime/startup-mode-flags';
|
||||
import type { VisibleOverlayInteractionRuntime } from '../runtime/visible-overlay-interaction-runtime';
|
||||
import type { AppState } from '../state';
|
||||
import type { AnilistAssembly } from './anilist';
|
||||
import type { FirstRunSetupAssembly } from './first-run-setup';
|
||||
import type { JellyfinAssembly } from './jellyfin';
|
||||
import type { MiningAssembly } from './mining';
|
||||
import type { OverlayAssembly } from './overlay';
|
||||
import type { PlaybackAssembly } from './playback';
|
||||
import type { SettingsAssembly } from './settings';
|
||||
import type { ShortcutsAssembly } from './shortcuts';
|
||||
import type { StatsAssembly } from './stats';
|
||||
import type { YomitanAssembly } from './yomitan';
|
||||
import type { YoutubeAssembly } from './youtube';
|
||||
|
||||
type AppReadyMainDeps = AppReadyComposerOptions['appReadyRuntimeMainDeps'];
|
||||
type OverlayManager = ReturnType<typeof createOverlayManager>;
|
||||
type FatalErrorReporter = (error: unknown, options: { title: string; context: string }) => void;
|
||||
|
||||
/**
|
||||
* What startup needs from the rest of the app. It touches nearly every domain, so most deps are
|
||||
* sibling assemblies; those built after this one are reached through getters.
|
||||
*/
|
||||
export interface StartupAssemblyDeps {
|
||||
appState: AppState;
|
||||
configService: Pick<
|
||||
ConfigService,
|
||||
'getConfig' | 'getConfigPath' | 'getWarnings' | 'reloadConfigStrict'
|
||||
>;
|
||||
logger: Pick<Logger, 'debug' | 'info' | 'warn' | 'error'>;
|
||||
logConfigWarning: AppReadyMainDeps['logConfigWarning'];
|
||||
configDir: string;
|
||||
userDataPath: string;
|
||||
defaultTexthookerPort: number;
|
||||
/** Electron's `dialog.showErrorBox`, injected so the assembly loads under `bun test`. */
|
||||
showErrorBox: (title: string, content: string) => void;
|
||||
reportFatalError: FatalErrorReporter;
|
||||
requestAppQuit: () => void;
|
||||
logging: Pick<AppReadyMainDeps, 'setLogLevel' | 'setLogRotation' | 'setLogFileToggles'>;
|
||||
overlayManager: Pick<OverlayManager, 'getVisibleOverlayVisible' | 'getOverlayWindows'>;
|
||||
overlayModalRuntime: Pick<OverlayModalRuntime, 'primeModalWindow'>;
|
||||
subtitleWsService: Pick<SubtitleWebSocket, 'start'>;
|
||||
annotationSubtitleWsService: Pick<SubtitleWebSocket, 'start'>;
|
||||
texthookerService: Pick<Texthooker, 'isRunning' | 'start'>;
|
||||
configDerivedRuntime: Pick<
|
||||
ReturnType<typeof createConfigDerivedRuntime>,
|
||||
'shouldAutoInitializeOverlayRuntimeFromConfig'
|
||||
>;
|
||||
hasMpvWebsocketPlugin: () => boolean;
|
||||
loadSubtitlePosition: () => void;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
syncOverlayMpvSubtitleSuppression: () => void;
|
||||
yomitan: Pick<YomitanAssembly, 'loadExtension' | 'ensureExtensionLoaded'>;
|
||||
stats: Pick<StatsAssembly, 'ensureImmersionTrackerStarted' | 'immersionTrackerStartupDeps'>;
|
||||
firstRunSetup: Pick<FirstRunSetupAssembly, 'service' | 'openWindow'>;
|
||||
youtube: Pick<
|
||||
YoutubeAssembly,
|
||||
| 'getCachedMediaPathForCurrentPlayback'
|
||||
| 'shouldRequireMediaCacheForCurrentPlayback'
|
||||
| 'getCurrentMediaCacheSourceUrl'
|
||||
>;
|
||||
overlay: Pick<
|
||||
OverlayAssembly,
|
||||
| 'overlayVisibilityRuntime'
|
||||
| 'createMainWindow'
|
||||
| 'setVisibleOverlayVisible'
|
||||
| 'ensureOverlayWindowsReadyForVisibilityActions'
|
||||
>;
|
||||
shortcuts: Pick<
|
||||
ShortcutsAssembly,
|
||||
| 'overlayShortcutsRuntime'
|
||||
| 'refreshCurrentSessionBindings'
|
||||
| 'registerGlobalShortcuts'
|
||||
| 'refreshOverlayShortcuts'
|
||||
>;
|
||||
settings: { hotReload: Pick<SettingsAssembly['hotReload'], 'start'> };
|
||||
mining: Pick<MiningAssembly, 'attachToAnkiIntegration' | 'createFieldGroupingCallback'>;
|
||||
getAnilist: () => Pick<AnilistAssembly, 'refreshClientSecretStateIfEnabled'>;
|
||||
getJellyfin: () => Pick<JellyfinAssembly, 'startJellyfinRemoteSession'>;
|
||||
getPlayback: () => Pick<
|
||||
PlaybackAssembly,
|
||||
| 'createMpvClient'
|
||||
| 'createMecabTokenizerAndCheck'
|
||||
| 'prewarmSubtitleDictionaries'
|
||||
| 'startBackgroundWarmups'
|
||||
| 'subtitleProcessingController'
|
||||
| 'getSubtitlePrefetchService'
|
||||
| 'getLastObservedTimePos'
|
||||
>;
|
||||
getPluginHost: () => { loadAll: () => Promise<unknown> };
|
||||
getOverlayNotifications: () => Pick<
|
||||
ReturnType<typeof createOverlayNotificationsRuntime>,
|
||||
'showOverlayNotification' | 'dismissOverlayNotification'
|
||||
>;
|
||||
getOverlayVisibilityComposer: () => Pick<
|
||||
ReturnType<typeof composeOverlayVisibilityRuntime>,
|
||||
'broadcastRuntimeOptionsChanged'
|
||||
>;
|
||||
/** Pointer interaction and geometry stay in `main.ts`: both import Electron at module load. */
|
||||
getVisibleOverlayInteraction: () => Pick<
|
||||
VisibleOverlayInteractionRuntime,
|
||||
'createOverlayWindowTracker' | 'bindVisibleOverlayOwner' | 'releaseVisibleOverlayOwner'
|
||||
>;
|
||||
getOverlayGeometry: () => Pick<
|
||||
ReturnType<typeof createOverlayGeometryRuntime>,
|
||||
'updateVisibleOverlayBounds'
|
||||
>;
|
||||
/** Runs the CLI command the app was launched with. */
|
||||
handleInitialArgs: () => void;
|
||||
}
|
||||
|
||||
export type StartupAssembly = ReturnType<typeof createStartupAssembly>;
|
||||
|
||||
/**
|
||||
* Owns how the app starts: the app-ready sequence, the headless known-word refresh, and bringing
|
||||
* up the overlay runtime and its prerequisites on demand (CLI commands, YouTube playback, the
|
||||
* tray). `main.ts` hands `runAppReady` to the app lifecycle.
|
||||
*/
|
||||
export function createStartupAssembly(deps: StartupAssemblyDeps) {
|
||||
const { appState, configService, logger } = deps;
|
||||
const isHeadlessLaunch = (): boolean =>
|
||||
Boolean(appState.initialArgs && isHeadlessInitialCommand(appState.initialArgs));
|
||||
const knownWordCacheStatePath = path.join(deps.userDataPath, 'known-words-cache.json');
|
||||
|
||||
function createRuntimeOptionsManager(): RuntimeOptionsManager {
|
||||
return new RuntimeOptionsManager(() => configService.getConfig().ankiConnect, {
|
||||
applyAnkiPatch: (patch) => {
|
||||
if (appState.ankiIntegration) {
|
||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
getSubtitleStyleConfig: () => configService.getConfig().subtitleStyle,
|
||||
onOptionsChanged: () => {
|
||||
const playback = deps.getPlayback();
|
||||
playback.subtitleProcessingController.invalidateTokenizationCache();
|
||||
playback.getSubtitlePrefetchService()?.onSeek(playback.getLastObservedTimePos());
|
||||
deps.getOverlayVisibilityComposer().broadcastRuntimeOptionsChanged();
|
||||
deps.shortcuts.refreshOverlayShortcuts();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resolveSessionKeybindings(): void {
|
||||
appState.keybindings = resolveKeybindings(configService.getConfig(), DEFAULT_KEYBINDINGS);
|
||||
deps.shortcuts.refreshCurrentSessionBindings();
|
||||
}
|
||||
|
||||
/** What the subtitle websockets send a client that connects mid-line. */
|
||||
function getCurrentSubtitleForWebsocket() {
|
||||
return (
|
||||
appState.currentSubtitleData ??
|
||||
(appState.currentSubText
|
||||
? {
|
||||
text: appState.currentSubText,
|
||||
tokens: null,
|
||||
startTime: appState.mpvClient?.currentSubStart ?? null,
|
||||
endTime: appState.mpvClient?.currentSubEnd ?? null,
|
||||
}
|
||||
: null)
|
||||
);
|
||||
}
|
||||
function getFrequencyOptionsForWebsocket() {
|
||||
const frequencyDictionary = configService.getConfig().subtitleStyle.frequencyDictionary;
|
||||
return {
|
||||
enabled: frequencyDictionary.enabled,
|
||||
topX: frequencyDictionary.topX,
|
||||
mode: frequencyDictionary.mode,
|
||||
};
|
||||
}
|
||||
|
||||
const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
||||
createOverlayRuntimeBootstrapHandlers({
|
||||
initializeOverlayRuntimeMainDeps: {
|
||||
appState,
|
||||
overlayManager: {
|
||||
getVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(),
|
||||
},
|
||||
overlayVisibilityRuntime: {
|
||||
updateVisibleOverlayVisibility: () =>
|
||||
deps.overlay.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
},
|
||||
refreshCurrentSubtitle: () => {
|
||||
deps
|
||||
.getPlayback()
|
||||
.subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
|
||||
},
|
||||
overlayShortcutsRuntime: {
|
||||
syncOverlayShortcuts: () => deps.shortcuts.overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||
},
|
||||
createMainWindow: () => {
|
||||
if (isHeadlessLaunch()) {
|
||||
return;
|
||||
}
|
||||
deps.overlay.createMainWindow();
|
||||
},
|
||||
registerGlobalShortcuts: () => {
|
||||
if (isHeadlessLaunch()) {
|
||||
return;
|
||||
}
|
||||
deps.shortcuts.registerGlobalShortcuts();
|
||||
},
|
||||
createWindowTracker: (override, targetMpvSocketPath) =>
|
||||
deps
|
||||
.getVisibleOverlayInteraction()
|
||||
.createOverlayWindowTracker(override, targetMpvSocketPath),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.getOverlayGeometry().updateVisibleOverlayBounds(geometry),
|
||||
bindOverlayOwner: () => deps.getVisibleOverlayInteraction().bindVisibleOverlayOwner(),
|
||||
releaseOverlayOwner: () => deps.getVisibleOverlayInteraction().releaseVisibleOverlayOwner(),
|
||||
getOverlayWindows: () => deps.overlayManager.getOverlayWindows(),
|
||||
getResolvedConfig: () => configService.getConfig(),
|
||||
showDesktopNotification,
|
||||
showOverlayNotification: (payload) =>
|
||||
deps.getOverlayNotifications().showOverlayNotification(payload),
|
||||
dismissOverlayNotification: (id) =>
|
||||
deps.getOverlayNotifications().dismissOverlayNotification(id),
|
||||
createFieldGroupingCallback: () => deps.mining.createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => knownWordCacheStatePath,
|
||||
getCachedMediaPath: (currentVideoPath, kind) =>
|
||||
deps.youtube.getCachedMediaPathForCurrentPlayback(currentVideoPath, kind),
|
||||
shouldRequireRemoteMediaCache: () =>
|
||||
deps.youtube.shouldRequireMediaCacheForCurrentPlayback(),
|
||||
getYoutubeMediaSourceUrl: () => deps.youtube.getCurrentMediaCacheSourceUrl(),
|
||||
shouldStartAnkiIntegration: () => !isHeadlessLaunch(),
|
||||
},
|
||||
initializeOverlayRuntimeBootstrapDeps: {
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlayRuntimeCore,
|
||||
setOverlayRuntimeInitialized: (initialized) => {
|
||||
appState.overlayRuntimeInitialized = initialized;
|
||||
},
|
||||
startBackgroundWarmups: () => {
|
||||
if (isHeadlessLaunch()) {
|
||||
return;
|
||||
}
|
||||
deps.getPlayback().startBackgroundWarmups();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** Creates the overlay windows and runtime once, then wires the Anki integration it built. */
|
||||
function initializeOverlayRuntime(): void {
|
||||
initializeOverlayRuntimeHandler();
|
||||
if (!isHeadlessLaunch()) {
|
||||
deps.overlayModalRuntime.primeModalWindow();
|
||||
}
|
||||
if (appState.ankiIntegration) {
|
||||
deps.mining.attachToAnkiIntegration(appState.ankiIntegration);
|
||||
}
|
||||
deps.syncOverlayMpvSubtitleSuppression();
|
||||
}
|
||||
|
||||
/** Creates whatever overlay commands need that app-ready skipped on a deferred start. */
|
||||
function ensureOverlayStartupPrereqs(): void {
|
||||
if (appState.subtitlePosition === null) {
|
||||
deps.loadSubtitlePosition();
|
||||
}
|
||||
if (appState.keybindings.length === 0) {
|
||||
resolveSessionKeybindings();
|
||||
} else if (!appState.sessionBindingsInitialized) {
|
||||
deps.shortcuts.refreshCurrentSessionBindings();
|
||||
}
|
||||
if (!appState.mpvClient) {
|
||||
appState.mpvClient = deps.getPlayback().createMpvClient();
|
||||
}
|
||||
if (!appState.runtimeOptionsManager) {
|
||||
appState.runtimeOptionsManager = createRuntimeOptionsManager();
|
||||
}
|
||||
if (!appState.subtitleTimingTracker) {
|
||||
appState.subtitleTimingTracker = new SubtitleTimingTracker();
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureYoutubePlaybackRuntimeReady(): Promise<void> {
|
||||
ensureOverlayStartupPrereqs();
|
||||
await deps.yomitan.ensureExtensionLoaded();
|
||||
if (!appState.overlayRuntimeInitialized) {
|
||||
initializeOverlayRuntime();
|
||||
return;
|
||||
}
|
||||
deps.overlay.ensureOverlayWindowsReadyForVisibilityActions();
|
||||
}
|
||||
|
||||
/** `--refresh-known-words` without the overlay: refresh the cache, then quit. */
|
||||
async function runHeadlessInitialCommand(): Promise<void> {
|
||||
if (!appState.initialArgs?.refreshKnownWords) {
|
||||
deps.handleInitialArgs();
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedConfig = configService.getConfig();
|
||||
if (resolvedConfig.ankiConnect.enabled !== true) {
|
||||
logger.error('Headless known-word refresh failed: AnkiConnect integration not enabled');
|
||||
process.exitCode = 1;
|
||||
deps.requestAppQuit();
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveAnkiConfig =
|
||||
appState.runtimeOptionsManager?.getEffectiveAnkiConnectConfig(resolvedConfig.ankiConnect) ??
|
||||
resolvedConfig.ankiConnect;
|
||||
const integration = new AnkiIntegration(
|
||||
effectiveAnkiConfig,
|
||||
new SubtitleTimingTracker(),
|
||||
{ send: () => undefined } as never,
|
||||
undefined,
|
||||
undefined,
|
||||
async () => ({
|
||||
keepNoteId: 0,
|
||||
deleteNoteId: 0,
|
||||
deleteDuplicate: false,
|
||||
cancelled: true,
|
||||
}),
|
||||
knownWordCacheStatePath,
|
||||
mergeAiConfig(resolvedConfig.ai, resolvedConfig.ankiConnect?.ai),
|
||||
);
|
||||
|
||||
try {
|
||||
await integration.refreshKnownWordCache();
|
||||
} catch (error) {
|
||||
logger.error('Headless known-word refresh failed:', error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
integration.stop();
|
||||
deps.requestAppQuit();
|
||||
}
|
||||
}
|
||||
|
||||
const failHandlers = {
|
||||
logError: (details: string) => logger.error(details),
|
||||
showErrorBox: (title: string, details: string) => deps.showErrorBox(title, details),
|
||||
quit: () => deps.requestAppQuit(),
|
||||
};
|
||||
|
||||
const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||
reloadConfigMainDeps: {
|
||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||
logInfo: (message) => logger.info(message),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
logWarning: (message) => logger.warn(message),
|
||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||
startConfigHotReload: () => deps.settings.hotReload.start(),
|
||||
shouldRefreshAnilistClientSecretState: () =>
|
||||
shouldRefreshAnilistOnConfigReload(appState.initialArgs),
|
||||
refreshAnilistClientSecretState: (options) =>
|
||||
deps.getAnilist().refreshClientSecretStateIfEnabled(options),
|
||||
failHandlers,
|
||||
},
|
||||
criticalConfigErrorMainDeps: {
|
||||
getConfigPath: () => configService.getConfigPath(),
|
||||
failHandlers,
|
||||
},
|
||||
appReadyRuntimeMainDeps: {
|
||||
ensureDefaultConfigBootstrap: () => {
|
||||
ensureDefaultConfigBootstrap({
|
||||
configDir: deps.configDir,
|
||||
configFilePaths: getDefaultConfigFilePaths(deps.configDir),
|
||||
generateTemplate: () => generateConfigTemplate(DEFAULT_CONFIG),
|
||||
});
|
||||
},
|
||||
loadSubtitlePosition: () => deps.loadSubtitlePosition(),
|
||||
resolveKeybindings: () => resolveSessionKeybindings(),
|
||||
createMpvClient: () => {
|
||||
appState.mpvClient = deps.getPlayback().createMpvClient();
|
||||
},
|
||||
startPlugins: () => {
|
||||
void deps.getPluginHost().loadAll();
|
||||
},
|
||||
getResolvedConfig: () => configService.getConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => deps.logConfigWarning(warning),
|
||||
...deps.logging,
|
||||
initRuntimeOptionsManager: () => {
|
||||
appState.runtimeOptionsManager = createRuntimeOptionsManager();
|
||||
},
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
deps.setSecondarySubMode(mode);
|
||||
},
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||
defaultAnnotationWebsocketPort: DEFAULT_CONFIG.annotationWebsocket.port,
|
||||
defaultTexthookerPort: deps.defaultTexthookerPort,
|
||||
hasMpvWebsocketPlugin: () => deps.hasMpvWebsocketPlugin(),
|
||||
startSubtitleWebsocket: (port: number) => {
|
||||
deps.subtitleWsService.start(
|
||||
port,
|
||||
getCurrentSubtitleForWebsocket,
|
||||
getFrequencyOptionsForWebsocket,
|
||||
);
|
||||
},
|
||||
startAnnotationWebsocket: (port: number) => {
|
||||
deps.annotationSubtitleWsService.start(
|
||||
port,
|
||||
getCurrentSubtitleForWebsocket,
|
||||
getFrequencyOptionsForWebsocket,
|
||||
);
|
||||
},
|
||||
startTexthooker: (port: number, websocketUrl?: string) => {
|
||||
if (!deps.texthookerService.isRunning()) {
|
||||
deps.texthookerService.start(port, websocketUrl);
|
||||
}
|
||||
},
|
||||
log: (message) => logger.info(message),
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
await deps.getPlayback().createMecabTokenizerAndCheck();
|
||||
},
|
||||
createSubtitleTimingTracker: () => {
|
||||
appState.subtitleTimingTracker = new SubtitleTimingTracker();
|
||||
},
|
||||
loadYomitanExtension: async () => {
|
||||
await deps.yomitan.loadExtension();
|
||||
},
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
await deps.yomitan.ensureExtensionLoaded();
|
||||
},
|
||||
handleFirstRunSetup: async () => {
|
||||
const snapshot = await deps.firstRunSetup.service.ensureSetupStateInitialized();
|
||||
appState.firstRunSetupCompleted = snapshot.state.status === 'completed';
|
||||
const args = appState.initialArgs;
|
||||
if (args && shouldAutoOpenFirstRunSetup(args)) {
|
||||
const force = Boolean(args.setup);
|
||||
if (force || snapshot.state.status !== 'completed') {
|
||||
deps.firstRunSetup.openWindow(force);
|
||||
}
|
||||
}
|
||||
},
|
||||
startJellyfinRemoteSession: async () => {
|
||||
await deps.getJellyfin().startJellyfinRemoteSession();
|
||||
},
|
||||
prewarmSubtitleDictionaries: async () => {
|
||||
await deps.getPlayback().prewarmSubtitleDictionaries();
|
||||
},
|
||||
startBackgroundWarmups: () => {
|
||||
deps.getPlayback().startBackgroundWarmups();
|
||||
},
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
|
||||
appState.backgroundMode
|
||||
? false
|
||||
: deps.configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||
setVisibleOverlayVisible: (visible: boolean) =>
|
||||
deps.overlay.setVisibleOverlayVisible(visible),
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
runHeadlessInitialCommand: () => runHeadlessInitialCommand(),
|
||||
handleInitialArgs: () => deps.handleInitialArgs(),
|
||||
shouldRunHeadlessInitialCommand: () => isHeadlessLaunch(),
|
||||
shouldUseMinimalStartup: () =>
|
||||
getStartupModeFlags(appState.initialArgs).shouldUseMinimalStartup,
|
||||
shouldSkipHeavyStartup: () =>
|
||||
getStartupModeFlags(appState.initialArgs).shouldSkipHeavyStartup,
|
||||
shouldHandleInitialArgsBeforeDeferredOverlayWarmup: () =>
|
||||
shouldHandleInitialArgsBeforeDeferredOverlayWarmup(appState.initialArgs),
|
||||
createImmersionTracker: () => {
|
||||
deps.stats.ensureImmersionTrackerStarted();
|
||||
},
|
||||
logDebug: (message: string) => {
|
||||
logger.debug(message);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
},
|
||||
immersionTrackerStartupMainDeps: deps.stats.immersionTrackerStartupDeps,
|
||||
});
|
||||
|
||||
/** The app-ready sequence; a failure is reported as fatal and quits the app. */
|
||||
async function runAppReady(): Promise<void> {
|
||||
try {
|
||||
await appReadyRuntimeRunner();
|
||||
} catch (error) {
|
||||
deps.reportFatalError(error, {
|
||||
title: 'SubMiner startup failed',
|
||||
context: 'SubMiner failed during app-ready startup.',
|
||||
});
|
||||
process.exitCode = 1;
|
||||
deps.requestAppQuit();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
runAppReady,
|
||||
runHeadlessInitialCommand,
|
||||
initializeOverlayRuntime,
|
||||
ensureOverlayStartupPrereqs,
|
||||
ensureYoutubePlaybackRuntimeReady,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user