mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-30 06:12:06 -07:00
refactor: split main boot phases
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
---
|
||||
id: TASK-238.7
|
||||
title: Split src/main.ts into boot-phase services, runtimes, and handlers
|
||||
status: To Do
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-03-27 00:00'
|
||||
updated_date: '2026-03-27 22:45'
|
||||
labels:
|
||||
- tech-debt
|
||||
- runtime
|
||||
@@ -31,11 +32,11 @@ After the remaining inline runtime logic and composer gaps are extracted, `src/m
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Service instantiation lives in a dedicated boot module instead of a large inline setup block in `src/main.ts`.
|
||||
- [ ] #2 Domain runtime composition lives in a dedicated boot module, separate from lifecycle and handler dispatch.
|
||||
- [ ] #3 Handler/composer invocation lives in a dedicated boot module, with `src/main.ts` reduced to app lifecycle and startup-path selection.
|
||||
- [ ] #4 Existing startup behavior remains unchanged across desktop and headless flows.
|
||||
- [ ] #5 Focused tests cover the split surfaces, and the relevant runtime/typecheck gate passes.
|
||||
- [x] #1 Service instantiation lives in a dedicated boot module instead of a large inline setup block in `src/main.ts`.
|
||||
- [x] #2 Domain runtime composition lives in a dedicated boot module, separate from lifecycle and handler dispatch.
|
||||
- [x] #3 Handler/composer invocation lives in a dedicated boot module, with `src/main.ts` reduced to app lifecycle and startup-path selection.
|
||||
- [x] #4 Existing startup behavior remains unchanged across desktop and headless flows.
|
||||
- [x] #5 Focused tests cover the split surfaces, and the relevant runtime/typecheck gate passes.
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
@@ -56,3 +57,29 @@ Guardrails:
|
||||
- Prefer small boot modules with narrow ownership over a new monolithic bootstrap layer.
|
||||
- Do not reopen the inline logic work already tracked by TASK-238.6 unless a remaining seam truly belongs here.
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
Added boot-phase modules under `src/main/boot/`:
|
||||
`services.ts` for config/user-data/runtime-registry/overlay bootstrap service construction,
|
||||
`runtimes.ts` for named runtime/composer entrypoints and grouped boot-phase seams,
|
||||
and `handlers.ts` for handler/composer boot entrypoints.
|
||||
|
||||
Rewired `src/main.ts` to source boot-phase service construction from `createMainBootServices(...)` and to route runtime/handler composition through boot-level exports instead of keeping the entrypoint as the direct owner of every composition import.
|
||||
|
||||
Added focused tests for the new boot seams in
|
||||
`src/main/boot/services.test.ts`,
|
||||
`src/main/boot/runtimes.test.ts`,
|
||||
and `src/main/boot/handlers.test.ts`.
|
||||
|
||||
Updated internal architecture docs to note that `src/main/boot/` now owns boot-phase assembly seams so `src/main.ts` can stay centered on lifecycle coordination and startup-path selection.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
TASK-238.7 is complete. Verification passed with focused boot tests, `bun run typecheck`, `bun run test:fast`, and `bun run build`. `src/main.ts` still acts as the composition root, but the boot-phase split now moves service instantiation, runtime composition seams, and handler composition seams into dedicated `src/main/boot/*` modules so the entrypoint reads more like a lifecycle coordinator than a single monolithic bootstrap file.
|
||||
|
||||
Backlog completion now includes changelog artifact `changes/2026-03-27-task-238.7-main-boot-split.md` for the internal runtime architecture pass.
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
6
changes/2026-03-27-task-238.7-main-boot-split.md
Normal file
6
changes/2026-03-27-task-238.7-main-boot-split.md
Normal file
@@ -0,0 +1,6 @@
|
||||
type: internal
|
||||
area: runtime
|
||||
|
||||
- Split `src/main.ts` boot wiring into dedicated `src/main/boot/services.ts`, `src/main/boot/runtimes.ts`, and `src/main/boot/handlers.ts` modules.
|
||||
- Added focused tests for the new boot-phase seams and kept the startup/typecheck/build verification lanes green.
|
||||
- Updated internal architecture/task docs to record the boot-phase split and new ownership boundary.
|
||||
@@ -24,6 +24,7 @@ The desktop app keeps `src/main.ts` as composition root and pushes behavior into
|
||||
## Current Shape
|
||||
|
||||
- `src/main/` owns composition, runtime setup, IPC wiring, and app lifecycle adapters.
|
||||
- `src/main/boot/` owns boot-phase assembly seams so `src/main.ts` can stay focused on lifecycle coordination and startup-path selection.
|
||||
- `src/core/services/` owns focused runtime services plus pure or side-effect-bounded logic.
|
||||
- `src/renderer/` owns overlay rendering and input behavior.
|
||||
- `src/config/` owns config definitions, defaults, loading, and resolution.
|
||||
|
||||
331
src/main.ts
331
src/main.ts
@@ -401,6 +401,31 @@ import {
|
||||
import { handleMpvCommandFromIpcRuntime } from './main/ipc-mpv-command';
|
||||
import { registerIpcRuntimeServices } from './main/ipc-runtime';
|
||||
import { createAnkiJimakuIpcRuntimeServiceDeps } from './main/dependencies';
|
||||
import { createMainBootServices } from './main/boot/services';
|
||||
import {
|
||||
composeBootOverlayVisibilityRuntime,
|
||||
composeBootJellyfinRuntimeHandlers,
|
||||
composeBootAnilistSetupHandlers,
|
||||
createBootMaybeFocusExistingAnilistSetupWindowHandler,
|
||||
createBootBuildOpenAnilistSetupWindowMainDepsHandler,
|
||||
createBootOpenAnilistSetupWindowHandler,
|
||||
composeBootAnilistTrackingHandlers,
|
||||
composeBootStatsStartupRuntime,
|
||||
createBootRunStatsCliCommandHandler,
|
||||
composeBootAppReadyRuntime,
|
||||
composeBootMpvRuntimeHandlers,
|
||||
createBootTrayRuntimeHandlers,
|
||||
createBootYomitanProfilePolicy,
|
||||
createBootYomitanExtensionRuntime,
|
||||
createBootYomitanSettingsRuntime,
|
||||
} from './main/boot/runtimes';
|
||||
import {
|
||||
composeBootStartupLifecycleHandlers,
|
||||
composeBootIpcRuntimeHandlers,
|
||||
composeBootCliStartupHandlers,
|
||||
composeBootHeadlessStartupHandlers,
|
||||
composeBootOverlayWindowHandlers,
|
||||
} from './main/boot/handlers';
|
||||
import { handleCliCommandRuntimeServiceWithContext } from './main/cli-runtime';
|
||||
import { createOverlayModalRuntimeService } from './main/overlay-runtime';
|
||||
import { createOverlayModalInputState } from './main/runtime/overlay-modal-input-state';
|
||||
@@ -555,59 +580,6 @@ function applyJellyfinMpvDefaults(
|
||||
applyJellyfinMpvDefaultsHandler(client);
|
||||
}
|
||||
|
||||
const CONFIG_DIR = resolveConfigDir({
|
||||
platform: process.platform,
|
||||
appDataDir: process.env.APPDATA,
|
||||
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
||||
homeDir: os.homedir(),
|
||||
existsSync: fs.existsSync,
|
||||
});
|
||||
const USER_DATA_PATH = CONFIG_DIR;
|
||||
const DEFAULT_MPV_LOG_PATH = process.env.SUBMINER_MPV_LOG?.trim() || DEFAULT_MPV_LOG_FILE;
|
||||
const DEFAULT_IMMERSION_DB_PATH = path.join(USER_DATA_PATH, 'immersion.sqlite');
|
||||
const configService = (() => {
|
||||
try {
|
||||
return new ConfigService(CONFIG_DIR);
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigStartupParseError) {
|
||||
failStartupFromConfig(
|
||||
'SubMiner config parse error',
|
||||
buildConfigParseErrorDetails(error.path, error.parseError),
|
||||
{
|
||||
logError: (details) => console.error(details),
|
||||
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
|
||||
quit: () => requestAppQuit(),
|
||||
},
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
const anilistTokenStore = createAnilistTokenStore(
|
||||
path.join(USER_DATA_PATH, ANILIST_TOKEN_STORE_FILE),
|
||||
{
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
warnUser: (message: string) => notifyAnilistTokenStoreWarning(message),
|
||||
},
|
||||
);
|
||||
const jellyfinTokenStore = createJellyfinTokenStore(
|
||||
path.join(USER_DATA_PATH, JELLYFIN_TOKEN_STORE_FILE),
|
||||
{
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
},
|
||||
);
|
||||
const anilistUpdateQueue = createAnilistUpdateQueue(
|
||||
path.join(USER_DATA_PATH, ANILIST_RETRY_QUEUE_FILE),
|
||||
{
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
},
|
||||
);
|
||||
const isDev = process.argv.includes('--dev') || process.argv.includes('--debug');
|
||||
const texthookerService = new Texthooker(() => {
|
||||
const config = getResolvedConfig();
|
||||
@@ -644,9 +616,141 @@ const texthookerService = new Texthooker(() => {
|
||||
},
|
||||
};
|
||||
});
|
||||
const subtitleWsService = new SubtitleWebSocket();
|
||||
const annotationSubtitleWsService = new SubtitleWebSocket();
|
||||
const logger = createLogger('main');
|
||||
let syncOverlayShortcutsForModal: (isActive: boolean) => void = () => {};
|
||||
let syncOverlayVisibilityForModal: () => void = () => {};
|
||||
const buildGetDefaultSocketPathMainDepsHandler = createBuildGetDefaultSocketPathMainDepsHandler({
|
||||
platform: process.platform,
|
||||
});
|
||||
const getDefaultSocketPathMainDeps = buildGetDefaultSocketPathMainDepsHandler();
|
||||
const getDefaultSocketPathHandler = createGetDefaultSocketPathHandler(getDefaultSocketPathMainDeps);
|
||||
|
||||
function getDefaultSocketPath(): string {
|
||||
return getDefaultSocketPathHandler();
|
||||
}
|
||||
const bootServices = createMainBootServices({
|
||||
platform: process.platform,
|
||||
argv: process.argv,
|
||||
appDataDir: process.env.APPDATA,
|
||||
xdgConfigHome: process.env.XDG_CONFIG_HOME,
|
||||
homeDir: os.homedir(),
|
||||
defaultMpvLogFile: DEFAULT_MPV_LOG_FILE,
|
||||
envMpvLog: process.env.SUBMINER_MPV_LOG,
|
||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||
resolveConfigDir,
|
||||
existsSync: fs.existsSync,
|
||||
mkdirSync: fs.mkdirSync,
|
||||
joinPath: (...parts) => path.join(...parts),
|
||||
app,
|
||||
shouldBypassSingleInstanceLock: () => shouldBypassSingleInstanceLockForArgv(process.argv),
|
||||
requestSingleInstanceLockEarly: () => requestSingleInstanceLockEarly(app),
|
||||
registerSecondInstanceHandlerEarly: (listener) => {
|
||||
registerSecondInstanceHandlerEarly(app, listener);
|
||||
},
|
||||
onConfigStartupParseError: (error) => {
|
||||
failStartupFromConfig(
|
||||
'SubMiner config parse error',
|
||||
buildConfigParseErrorDetails(error.path, error.parseError),
|
||||
{
|
||||
logError: (details) => console.error(details),
|
||||
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
|
||||
quit: () => requestAppQuit(),
|
||||
},
|
||||
);
|
||||
},
|
||||
createConfigService: (configDir) => new ConfigService(configDir),
|
||||
createAnilistTokenStore: (targetPath) =>
|
||||
createAnilistTokenStore(targetPath, {
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
warnUser: (message: string) => notifyAnilistTokenStoreWarning(message),
|
||||
}),
|
||||
createJellyfinTokenStore: (targetPath) =>
|
||||
createJellyfinTokenStore(targetPath, {
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
}),
|
||||
createAnilistUpdateQueue: (targetPath) =>
|
||||
createAnilistUpdateQueue(targetPath, {
|
||||
info: (message: string) => console.info(message),
|
||||
warn: (message: string, details?: unknown) => console.warn(message, details),
|
||||
error: (message: string, details?: unknown) => console.error(message, details),
|
||||
}),
|
||||
createSubtitleWebSocket: () => new SubtitleWebSocket(),
|
||||
createLogger,
|
||||
createMainRuntimeRegistry,
|
||||
createOverlayManager,
|
||||
createOverlayModalInputState,
|
||||
createOverlayContentMeasurementStore: ({ logger }) => {
|
||||
const buildHandler = createBuildOverlayContentMeasurementStoreMainDepsHandler({
|
||||
now: () => Date.now(),
|
||||
warn: (message: string) => logger.warn(message),
|
||||
});
|
||||
return createOverlayContentMeasurementStore(buildHandler());
|
||||
},
|
||||
getSyncOverlayShortcutsForModal: () => syncOverlayShortcutsForModal,
|
||||
getSyncOverlayVisibilityForModal: () => syncOverlayVisibilityForModal,
|
||||
createOverlayModalRuntime: ({ overlayManager, overlayModalInputState }) => {
|
||||
const buildHandler = createBuildOverlayModalRuntimeMainDepsHandler({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getModalWindow: () => overlayManager.getModalWindow(),
|
||||
createModalWindow: () => createModalWindow(),
|
||||
getModalGeometry: () => getCurrentOverlayGeometry(),
|
||||
setModalWindowBounds: (geometry) => overlayManager.setModalWindowBounds(geometry),
|
||||
});
|
||||
return createOverlayModalRuntimeService(buildHandler(), {
|
||||
onModalStateChange: (isActive: boolean) =>
|
||||
overlayModalInputState.handleModalInputStateChange(isActive),
|
||||
});
|
||||
},
|
||||
createAppState,
|
||||
}) as {
|
||||
configDir: string;
|
||||
userDataPath: string;
|
||||
defaultMpvLogPath: string;
|
||||
defaultImmersionDbPath: string;
|
||||
configService: ConfigService;
|
||||
anilistTokenStore: ReturnType<typeof createAnilistTokenStore>;
|
||||
jellyfinTokenStore: ReturnType<typeof createJellyfinTokenStore>;
|
||||
anilistUpdateQueue: ReturnType<typeof createAnilistUpdateQueue>;
|
||||
subtitleWsService: SubtitleWebSocket;
|
||||
annotationSubtitleWsService: SubtitleWebSocket;
|
||||
logger: ReturnType<typeof createLogger>;
|
||||
runtimeRegistry: ReturnType<typeof createMainRuntimeRegistry>;
|
||||
overlayManager: ReturnType<typeof createOverlayManager>;
|
||||
overlayModalInputState: ReturnType<typeof createOverlayModalInputState>;
|
||||
overlayContentMeasurementStore: ReturnType<typeof createOverlayContentMeasurementStore>;
|
||||
overlayModalRuntime: ReturnType<typeof createOverlayModalRuntimeService>;
|
||||
appState: ReturnType<typeof createAppState>;
|
||||
appLifecycleApp: {
|
||||
requestSingleInstanceLock: () => boolean;
|
||||
quit: () => void;
|
||||
on: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
||||
whenReady: () => Promise<void>;
|
||||
};
|
||||
};
|
||||
const {
|
||||
configDir: CONFIG_DIR,
|
||||
userDataPath: USER_DATA_PATH,
|
||||
defaultMpvLogPath: DEFAULT_MPV_LOG_PATH,
|
||||
defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH,
|
||||
configService,
|
||||
anilistTokenStore,
|
||||
jellyfinTokenStore,
|
||||
anilistUpdateQueue,
|
||||
subtitleWsService,
|
||||
annotationSubtitleWsService,
|
||||
logger,
|
||||
runtimeRegistry,
|
||||
overlayManager,
|
||||
overlayModalInputState,
|
||||
overlayContentMeasurementStore,
|
||||
overlayModalRuntime,
|
||||
appState,
|
||||
appLifecycleApp,
|
||||
} = bootServices;
|
||||
notifyAnilistTokenStoreWarning = (message: string) => {
|
||||
logger.warn(`[AniList] ${message}`);
|
||||
try {
|
||||
@@ -681,41 +785,6 @@ const appLogger = {
|
||||
);
|
||||
},
|
||||
};
|
||||
const runtimeRegistry = createMainRuntimeRegistry();
|
||||
const appLifecycleApp = {
|
||||
requestSingleInstanceLock: () =>
|
||||
shouldBypassSingleInstanceLockForArgv(process.argv)
|
||||
? true
|
||||
: requestSingleInstanceLockEarly(app),
|
||||
quit: () => app.quit(),
|
||||
on: (event: string, listener: (...args: unknown[]) => void) => {
|
||||
if (event === 'second-instance') {
|
||||
registerSecondInstanceHandlerEarly(
|
||||
app,
|
||||
listener as (_event: unknown, argv: string[]) => void,
|
||||
);
|
||||
return app;
|
||||
}
|
||||
app.on(event as Parameters<typeof app.on>[0], listener as (...args: any[]) => void);
|
||||
return app;
|
||||
},
|
||||
whenReady: () => app.whenReady(),
|
||||
};
|
||||
|
||||
const buildGetDefaultSocketPathMainDepsHandler = createBuildGetDefaultSocketPathMainDepsHandler({
|
||||
platform: process.platform,
|
||||
});
|
||||
const getDefaultSocketPathMainDeps = buildGetDefaultSocketPathMainDepsHandler();
|
||||
const getDefaultSocketPathHandler = createGetDefaultSocketPathHandler(getDefaultSocketPathMainDeps);
|
||||
|
||||
function getDefaultSocketPath(): string {
|
||||
return getDefaultSocketPathHandler();
|
||||
}
|
||||
|
||||
if (!fs.existsSync(USER_DATA_PATH)) {
|
||||
fs.mkdirSync(USER_DATA_PATH, { recursive: true });
|
||||
}
|
||||
app.setPath('userData', USER_DATA_PATH);
|
||||
|
||||
let forceQuitTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let statsServer: ReturnType<typeof startStatsServer> | null = null;
|
||||
@@ -777,46 +846,6 @@ process.on('SIGTERM', () => {
|
||||
requestAppQuit();
|
||||
});
|
||||
|
||||
const overlayManager = createOverlayManager();
|
||||
let syncOverlayShortcutsForModal: (isActive: boolean) => void = () => {};
|
||||
let syncOverlayVisibilityForModal: () => void = () => {};
|
||||
const overlayModalInputState = createOverlayModalInputState({
|
||||
getModalWindow: () => overlayManager.getModalWindow(),
|
||||
syncOverlayShortcutsForModal: (isActive) => {
|
||||
syncOverlayShortcutsForModal(isActive);
|
||||
},
|
||||
syncOverlayVisibilityForModal: () => {
|
||||
syncOverlayVisibilityForModal();
|
||||
},
|
||||
});
|
||||
|
||||
const buildOverlayContentMeasurementStoreMainDepsHandler =
|
||||
createBuildOverlayContentMeasurementStoreMainDepsHandler({
|
||||
now: () => Date.now(),
|
||||
warn: (message: string) => logger.warn(message),
|
||||
});
|
||||
const buildOverlayModalRuntimeMainDepsHandler = createBuildOverlayModalRuntimeMainDepsHandler({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getModalWindow: () => overlayManager.getModalWindow(),
|
||||
createModalWindow: () => createModalWindow(),
|
||||
getModalGeometry: () => getCurrentOverlayGeometry(),
|
||||
setModalWindowBounds: (geometry) => overlayManager.setModalWindowBounds(geometry),
|
||||
});
|
||||
const overlayContentMeasurementStoreMainDeps = buildOverlayContentMeasurementStoreMainDepsHandler();
|
||||
const overlayContentMeasurementStore = createOverlayContentMeasurementStore(
|
||||
overlayContentMeasurementStoreMainDeps,
|
||||
);
|
||||
const overlayModalRuntime = createOverlayModalRuntimeService(
|
||||
buildOverlayModalRuntimeMainDepsHandler(),
|
||||
{
|
||||
onModalStateChange: (isActive: boolean) =>
|
||||
overlayModalInputState.handleModalInputStateChange(isActive),
|
||||
},
|
||||
);
|
||||
const appState = createAppState({
|
||||
mpvSocketPath: getDefaultSocketPath(),
|
||||
texthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
});
|
||||
const startBackgroundWarmupsIfAllowed = (): void => {
|
||||
startBackgroundWarmups();
|
||||
};
|
||||
@@ -1887,7 +1916,7 @@ const buildOpenRuntimeOptionsPaletteMainDepsHandler =
|
||||
createBuildOpenRuntimeOptionsPaletteMainDepsHandler({
|
||||
openRuntimeOptionsPaletteRuntime: () => overlayModalRuntime.openRuntimeOptionsPalette(),
|
||||
});
|
||||
const overlayVisibilityComposer = composeOverlayVisibilityRuntime({
|
||||
const overlayVisibilityComposer = composeBootOverlayVisibilityRuntime({
|
||||
overlayVisibilityRuntime,
|
||||
restorePreviousSecondarySubVisibilityMainDeps:
|
||||
buildRestorePreviousSecondarySubVisibilityMainDepsHandler(),
|
||||
@@ -1959,7 +1988,7 @@ const {
|
||||
stopJellyfinRemoteSession,
|
||||
runJellyfinCommand,
|
||||
openJellyfinSetupWindow,
|
||||
} = composeJellyfinRuntimeHandlers({
|
||||
} = composeBootJellyfinRuntimeHandlers({
|
||||
getResolvedJellyfinConfigMainDeps: {
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
loadStoredSession: () => jellyfinTokenStore.loadSession(),
|
||||
@@ -2259,7 +2288,7 @@ const {
|
||||
consumeAnilistSetupTokenFromUrl,
|
||||
handleAnilistSetupProtocolUrl,
|
||||
registerSubminerProtocolClient,
|
||||
} = composeAnilistSetupHandlers({
|
||||
} = composeBootAnilistSetupHandlers({
|
||||
notifyDeps: {
|
||||
hasMpvClient: () => Boolean(appState.mpvClient),
|
||||
showMpvOsd: (message) => showMpvOsd(message),
|
||||
@@ -2310,10 +2339,10 @@ const {
|
||||
},
|
||||
});
|
||||
|
||||
const maybeFocusExistingAnilistSetupWindow = createMaybeFocusExistingAnilistSetupWindowHandler({
|
||||
const maybeFocusExistingAnilistSetupWindow = createBootMaybeFocusExistingAnilistSetupWindowHandler({
|
||||
getSetupWindow: () => appState.anilistSetupWindow,
|
||||
});
|
||||
const buildOpenAnilistSetupWindowMainDepsHandler = createBuildOpenAnilistSetupWindowMainDepsHandler(
|
||||
const buildOpenAnilistSetupWindowMainDepsHandler = createBootBuildOpenAnilistSetupWindowMainDepsHandler(
|
||||
{
|
||||
maybeFocusExistingSetupWindow: maybeFocusExistingAnilistSetupWindow,
|
||||
createSetupWindow: createCreateAnilistSetupWindowHandler({
|
||||
@@ -2361,7 +2390,7 @@ const buildOpenAnilistSetupWindowMainDepsHandler = createBuildOpenAnilistSetupWi
|
||||
);
|
||||
|
||||
function openAnilistSetupWindow(): void {
|
||||
createOpenAnilistSetupWindowHandler(buildOpenAnilistSetupWindowMainDepsHandler())();
|
||||
createBootOpenAnilistSetupWindowHandler(buildOpenAnilistSetupWindowMainDepsHandler())();
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -2375,7 +2404,7 @@ const {
|
||||
ensureAnilistMediaGuess,
|
||||
processNextAnilistRetryUpdate,
|
||||
maybeRunAnilistPostWatchUpdate,
|
||||
} = composeAnilistTrackingHandlers({
|
||||
} = composeBootAnilistTrackingHandlers({
|
||||
refreshClientSecretMainDeps: {
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
|
||||
@@ -2642,7 +2671,7 @@ const {
|
||||
onWillQuitCleanup: onWillQuitCleanupHandler,
|
||||
shouldRestoreWindowsOnActivate: shouldRestoreWindowsOnActivateHandler,
|
||||
restoreWindowsOnActivate: restoreWindowsOnActivateHandler,
|
||||
} = composeStartupLifecycleHandlers({
|
||||
} = composeBootStartupLifecycleHandlers({
|
||||
registerProtocolUrlHandlersMainDeps: {
|
||||
registerOpenUrl: (listener) => {
|
||||
app.on('open-url', listener);
|
||||
@@ -2950,7 +2979,7 @@ const ensureImmersionTrackerStarted = (): void => {
|
||||
hasAttemptedImmersionTrackerStartup = true;
|
||||
createImmersionTrackerStartup();
|
||||
};
|
||||
const statsStartupRuntime = composeStatsStartupRuntime({
|
||||
const statsStartupRuntime = composeBootStatsStartupRuntime({
|
||||
ensureStatsServerStarted: () => ensureStatsServerStarted(),
|
||||
ensureBackgroundStatsServerStarted: () => ensureBackgroundStatsServerStarted(),
|
||||
stopBackgroundStatsServer: () => stopBackgroundStatsServer(),
|
||||
@@ -2964,7 +2993,7 @@ const statsStartupRuntime = composeStatsStartupRuntime({
|
||||
},
|
||||
});
|
||||
|
||||
const runStatsCliCommand = createRunStatsCliCommandHandler({
|
||||
const runStatsCliCommand = createBootRunStatsCliCommandHandler({
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
ensureImmersionTrackerStarted: () => statsStartupRuntime.ensureImmersionTrackerStarted(),
|
||||
ensureVocabularyCleanupTokenizerReady: async () => {
|
||||
@@ -3031,7 +3060,7 @@ async function runHeadlessInitialCommand(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||
const { appReadyRuntimeRunner } = composeBootAppReadyRuntime({
|
||||
reloadConfigMainDeps: {
|
||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||
logInfo: (message) => appLogger.logInfo(message),
|
||||
@@ -3266,7 +3295,7 @@ const {
|
||||
startBackgroundWarmups,
|
||||
startTokenizationWarmups,
|
||||
isTokenizationWarmupReady,
|
||||
} = composeMpvRuntimeHandlers<
|
||||
} = composeBootMpvRuntimeHandlers<
|
||||
MpvIpcClient,
|
||||
ReturnType<typeof createTokenizerDepsRuntime>,
|
||||
SubtitleData
|
||||
@@ -4113,7 +4142,7 @@ const shiftSubtitleDelayToAdjacentCueHandler = createShiftSubtitleDelayToAdjacen
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
});
|
||||
|
||||
const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
||||
const { registerIpcRuntimeHandlers } = composeBootIpcRuntimeHandlers({
|
||||
mpvCommandMainDeps: {
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
@@ -4333,7 +4362,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
||||
registerIpcRuntimeServices,
|
||||
},
|
||||
});
|
||||
const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
|
||||
const { handleCliCommand, handleInitialArgs } = composeBootCliStartupHandlers({
|
||||
cliCommandContextMainDeps: {
|
||||
appState,
|
||||
setLogLevel: (level) => setLogLevel(level, 'cli'),
|
||||
@@ -4419,7 +4448,7 @@ const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
|
||||
logInfo: (message) => logger.info(message),
|
||||
},
|
||||
});
|
||||
const { runAndApplyStartupState } = composeHeadlessStartupHandlers<
|
||||
const { runAndApplyStartupState } = composeBootHeadlessStartupHandlers<
|
||||
CliArgs,
|
||||
StartupState,
|
||||
ReturnType<typeof createStartupBootstrapRuntimeDeps>
|
||||
@@ -4487,7 +4516,7 @@ if (isAnilistTrackingEnabled(getResolvedConfig())) {
|
||||
}
|
||||
void initializeDiscordPresenceService();
|
||||
const { createMainWindow: createMainWindowHandler, createModalWindow: createModalWindowHandler } =
|
||||
composeOverlayWindowHandlers<BrowserWindow>({
|
||||
composeBootOverlayWindowHandlers<BrowserWindow>({
|
||||
createOverlayWindowDeps: {
|
||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||
isDev,
|
||||
@@ -4513,7 +4542,7 @@ const { createMainWindow: createMainWindowHandler, createModalWindow: createModa
|
||||
setModalWindow: (window) => overlayManager.setModalWindow(window),
|
||||
});
|
||||
const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
||||
createTrayRuntimeHandlers({
|
||||
createBootTrayRuntimeHandlers({
|
||||
resolveTrayIconPathDeps: {
|
||||
resolveTrayIconPathRuntime,
|
||||
platform: process.platform,
|
||||
@@ -4560,12 +4589,12 @@ const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } =
|
||||
},
|
||||
buildMenuFromTemplate: (template) => Menu.buildFromTemplate(template),
|
||||
});
|
||||
const yomitanProfilePolicy = createYomitanProfilePolicy({
|
||||
const yomitanProfilePolicy = createBootYomitanProfilePolicy({
|
||||
externalProfilePath: getResolvedConfig().yomitan.externalProfilePath,
|
||||
logInfo: (message) => logger.info(message),
|
||||
});
|
||||
const configuredExternalYomitanProfilePath = yomitanProfilePolicy.externalProfilePath;
|
||||
const yomitanExtensionRuntime = createYomitanExtensionRuntime({
|
||||
const yomitanExtensionRuntime = createBootYomitanExtensionRuntime({
|
||||
loadYomitanExtensionCore,
|
||||
userDataPath: USER_DATA_PATH,
|
||||
externalProfilePath: configuredExternalYomitanProfilePath,
|
||||
@@ -4647,7 +4676,7 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
||||
},
|
||||
},
|
||||
});
|
||||
const { openYomitanSettings: openYomitanSettingsHandler } = createYomitanSettingsRuntime({
|
||||
const { openYomitanSettings: openYomitanSettingsHandler } = createBootYomitanSettingsRuntime({
|
||||
ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded(),
|
||||
getYomitanSession: () => appState.yomitanSession,
|
||||
openYomitanSettingsWindow: ({ yomitanExt, getExistingWindow, setWindow, yomitanSession }) => {
|
||||
|
||||
94
src/main/boot/handlers.test.ts
Normal file
94
src/main/boot/handlers.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createMainBootHandlers } from './handlers';
|
||||
|
||||
test('createMainBootHandlers returns grouped handler bundles', () => {
|
||||
const handlers = createMainBootHandlers<any, any, any, any>({
|
||||
startupLifecycleDeps: {
|
||||
registerProtocolUrlHandlersMainDeps: {} as never,
|
||||
onWillQuitCleanupMainDeps: {} as never,
|
||||
shouldRestoreWindowsOnActivateMainDeps: {} as never,
|
||||
restoreWindowsOnActivateMainDeps: {} as never,
|
||||
},
|
||||
ipcRuntimeDeps: {
|
||||
mpvCommandMainDeps: {} as never,
|
||||
handleMpvCommandFromIpcRuntime: () => ({ ok: true }) as never,
|
||||
runSubsyncManualFromIpc: () => Promise.resolve({ ok: true }) as never,
|
||||
registration: {
|
||||
runtimeOptions: {} as never,
|
||||
mainDeps: {} as never,
|
||||
ankiJimakuDeps: {} as never,
|
||||
registerIpcRuntimeServices: () => {},
|
||||
},
|
||||
},
|
||||
cliStartupDeps: {
|
||||
cliCommandContextMainDeps: {} as never,
|
||||
cliCommandRuntimeHandlerMainDeps: {} as never,
|
||||
initialArgsRuntimeHandlerMainDeps: {} as never,
|
||||
},
|
||||
headlessStartupDeps: {
|
||||
startupRuntimeHandlersDeps: {
|
||||
appLifecycleRuntimeRunnerMainDeps: {
|
||||
app: { on: () => {} } as never,
|
||||
platform: 'darwin',
|
||||
shouldStartApp: () => true,
|
||||
parseArgs: () => ({}) as never,
|
||||
handleCliCommand: () => {},
|
||||
printHelp: () => {},
|
||||
logNoRunningInstance: () => {},
|
||||
onReady: async () => {},
|
||||
onWillQuitCleanup: () => {},
|
||||
shouldRestoreWindowsOnActivate: () => false,
|
||||
restoreWindowsOnActivate: () => {},
|
||||
shouldQuitOnWindowAllClosed: () => false,
|
||||
},
|
||||
createAppLifecycleRuntimeRunner: () => () => {},
|
||||
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
|
||||
argv: ['node', 'main.js'],
|
||||
parseArgs: () => ({ command: 'start' }) as never,
|
||||
setLogLevel: () => {},
|
||||
forceX11Backend: () => {},
|
||||
enforceUnsupportedWaylandMode: () => {},
|
||||
shouldStartApp: () => true,
|
||||
getDefaultSocketPath: () => '/tmp/mpv.sock',
|
||||
defaultTexthookerPort: 5174,
|
||||
configDir: '/tmp/config',
|
||||
defaultConfig: {} as never,
|
||||
generateConfigTemplate: () => 'template',
|
||||
generateDefaultConfigFile: async () => 0,
|
||||
setExitCode: () => {},
|
||||
quitApp: () => {},
|
||||
logGenerateConfigError: () => {},
|
||||
startAppLifecycle: (args) => startAppLifecycle(args as never),
|
||||
}),
|
||||
createStartupBootstrapRuntimeDeps: (deps) => ({
|
||||
startAppLifecycle: deps.startAppLifecycle,
|
||||
}),
|
||||
runStartupBootstrapRuntime: () => ({ mode: 'started' } as never),
|
||||
applyStartupState: () => {},
|
||||
},
|
||||
},
|
||||
overlayWindowDeps: {
|
||||
createOverlayWindowDeps: {
|
||||
createOverlayWindowCore: (kind) => ({ kind }),
|
||||
isDev: false,
|
||||
ensureOverlayWindowLevel: () => {},
|
||||
onRuntimeOptionsChanged: () => {},
|
||||
setOverlayDebugVisualizationEnabled: () => {},
|
||||
isOverlayVisible: () => false,
|
||||
tryHandleOverlayShortcutLocalFallback: () => false,
|
||||
forwardTabToMpv: () => {},
|
||||
onWindowClosed: () => {},
|
||||
getYomitanSession: () => null,
|
||||
},
|
||||
setMainWindow: () => {},
|
||||
setModalWindow: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof handlers.startupLifecycle.registerProtocolUrlHandlers, 'function');
|
||||
assert.equal(typeof handlers.ipcRuntime.registerIpcRuntimeHandlers, 'function');
|
||||
assert.equal(typeof handlers.cliStartup.handleCliCommand, 'function');
|
||||
assert.equal(typeof handlers.headlessStartup.runAndApplyStartupState, 'function');
|
||||
assert.equal(typeof handlers.overlayWindow.createMainWindow, 'function');
|
||||
});
|
||||
40
src/main/boot/handlers.ts
Normal file
40
src/main/boot/handlers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { composeOverlayWindowHandlers } from '../runtime/composers/overlay-window-composer';
|
||||
import {
|
||||
composeCliStartupHandlers,
|
||||
composeHeadlessStartupHandlers,
|
||||
composeIpcRuntimeHandlers,
|
||||
composeStartupLifecycleHandlers,
|
||||
} from '../runtime/composers';
|
||||
|
||||
export interface MainBootHandlersParams<TBrowserWindow, TCliArgs, TStartupState, TBootstrapDeps> {
|
||||
startupLifecycleDeps: Parameters<typeof composeStartupLifecycleHandlers>[0];
|
||||
ipcRuntimeDeps: Parameters<typeof composeIpcRuntimeHandlers>[0];
|
||||
cliStartupDeps: Parameters<typeof composeCliStartupHandlers>[0];
|
||||
headlessStartupDeps: Parameters<
|
||||
typeof composeHeadlessStartupHandlers<TCliArgs, TStartupState, TBootstrapDeps>
|
||||
>[0];
|
||||
overlayWindowDeps: Parameters<typeof composeOverlayWindowHandlers<TBrowserWindow>>[0];
|
||||
}
|
||||
|
||||
export function createMainBootHandlers<
|
||||
TBrowserWindow,
|
||||
TCliArgs,
|
||||
TStartupState,
|
||||
TBootstrapDeps,
|
||||
>(params: MainBootHandlersParams<TBrowserWindow, TCliArgs, TStartupState, TBootstrapDeps>) {
|
||||
return {
|
||||
startupLifecycle: composeStartupLifecycleHandlers(params.startupLifecycleDeps),
|
||||
ipcRuntime: composeIpcRuntimeHandlers(params.ipcRuntimeDeps),
|
||||
cliStartup: composeCliStartupHandlers(params.cliStartupDeps),
|
||||
headlessStartup: composeHeadlessStartupHandlers<TCliArgs, TStartupState, TBootstrapDeps>(
|
||||
params.headlessStartupDeps,
|
||||
),
|
||||
overlayWindow: composeOverlayWindowHandlers<TBrowserWindow>(params.overlayWindowDeps),
|
||||
};
|
||||
}
|
||||
|
||||
export const composeBootStartupLifecycleHandlers = composeStartupLifecycleHandlers;
|
||||
export const composeBootIpcRuntimeHandlers = composeIpcRuntimeHandlers;
|
||||
export const composeBootCliStartupHandlers = composeCliStartupHandlers;
|
||||
export const composeBootHeadlessStartupHandlers = composeHeadlessStartupHandlers;
|
||||
export const composeBootOverlayWindowHandlers = composeOverlayWindowHandlers;
|
||||
339
src/main/boot/runtimes.test.ts
Normal file
339
src/main/boot/runtimes.test.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createMainBootRuntimes } from './runtimes';
|
||||
|
||||
test('createMainBootRuntimes returns grouped runtime bundles', () => {
|
||||
const runtimes = createMainBootRuntimes<any, any, any, any>({
|
||||
overlayVisibilityRuntimeDeps: {
|
||||
overlayVisibilityRuntime: {} as never,
|
||||
restorePreviousSecondarySubVisibilityMainDeps: {} as never,
|
||||
broadcastRuntimeOptionsChangedMainDeps: {} as never,
|
||||
sendToActiveOverlayWindowMainDeps: {} as never,
|
||||
setOverlayDebugVisualizationEnabledMainDeps: {} as never,
|
||||
openRuntimeOptionsPaletteMainDeps: {} as never,
|
||||
},
|
||||
jellyfinRuntimeHandlerDeps: {
|
||||
getResolvedJellyfinConfigMainDeps: {} as never,
|
||||
getJellyfinClientInfoMainDeps: {} as never,
|
||||
waitForMpvConnectedMainDeps: {} as never,
|
||||
launchMpvIdleForJellyfinPlaybackMainDeps: {} as never,
|
||||
ensureMpvConnectedForJellyfinPlaybackMainDeps: {} as never,
|
||||
preloadJellyfinExternalSubtitlesMainDeps: {} as never,
|
||||
playJellyfinItemInMpvMainDeps: {} as never,
|
||||
remoteComposerOptions: {} as never,
|
||||
handleJellyfinAuthCommandsMainDeps: {} as never,
|
||||
handleJellyfinListCommandsMainDeps: {} as never,
|
||||
handleJellyfinPlayCommandMainDeps: {} as never,
|
||||
handleJellyfinRemoteAnnounceCommandMainDeps: {} as never,
|
||||
startJellyfinRemoteSessionMainDeps: {} as never,
|
||||
stopJellyfinRemoteSessionMainDeps: {} as never,
|
||||
runJellyfinCommandMainDeps: {} as never,
|
||||
maybeFocusExistingJellyfinSetupWindowMainDeps: {} as never,
|
||||
openJellyfinSetupWindowMainDeps: {} as never,
|
||||
},
|
||||
anilistSetupDeps: {
|
||||
notifyDeps: {} as never,
|
||||
consumeTokenDeps: {} as never,
|
||||
handleProtocolDeps: {} as never,
|
||||
registerProtocolClientDeps: {} as never,
|
||||
},
|
||||
buildOpenAnilistSetupWindowMainDeps: {
|
||||
maybeFocusExistingSetupWindow: () => false,
|
||||
createSetupWindow: () => null as never,
|
||||
buildAuthorizeUrl: () => 'https://example.test',
|
||||
consumeCallbackUrl: () => false,
|
||||
openSetupInBrowser: () => {},
|
||||
loadManualTokenEntry: () => Promise.resolve(),
|
||||
redirectUri: 'https://example.test/callback',
|
||||
developerSettingsUrl: 'https://example.test/dev',
|
||||
isAllowedExternalUrl: () => true,
|
||||
isAllowedNavigationUrl: () => true,
|
||||
logWarn: () => {},
|
||||
logError: () => {},
|
||||
clearSetupWindow: () => {},
|
||||
setSetupPageOpened: () => {},
|
||||
setSetupWindow: () => {},
|
||||
openExternal: () => {},
|
||||
},
|
||||
anilistTrackingDeps: {
|
||||
refreshClientSecretMainDeps: {} as never,
|
||||
getCurrentMediaKeyMainDeps: {} as never,
|
||||
resetMediaTrackingMainDeps: {} as never,
|
||||
getMediaGuessRuntimeStateMainDeps: {} as never,
|
||||
setMediaGuessRuntimeStateMainDeps: {} as never,
|
||||
resetMediaGuessStateMainDeps: {} as never,
|
||||
maybeProbeDurationMainDeps: {} as never,
|
||||
ensureMediaGuessMainDeps: {} as never,
|
||||
processNextRetryUpdateMainDeps: {} as never,
|
||||
maybeRunPostWatchUpdateMainDeps: {} as never,
|
||||
},
|
||||
statsStartupRuntimeDeps: {
|
||||
ensureStatsServerStarted: () => '',
|
||||
ensureBackgroundStatsServerStarted: () => ({ url: '', runningInCurrentProcess: false }),
|
||||
stopBackgroundStatsServer: async () => ({ ok: true, stale: false }),
|
||||
ensureImmersionTrackerStarted: () => {},
|
||||
},
|
||||
runStatsCliCommandDeps: {
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
ensureImmersionTrackerStarted: () => {},
|
||||
ensureVocabularyCleanupTokenizerReady: async () => {},
|
||||
getImmersionTracker: () => null,
|
||||
ensureStatsServerStarted: () => '',
|
||||
ensureBackgroundStatsServerStarted: () => ({ url: '', runningInCurrentProcess: false }),
|
||||
stopBackgroundStatsServer: async () => ({ ok: true, stale: false }),
|
||||
openExternal: () => Promise.resolve(),
|
||||
writeResponse: () => {},
|
||||
exitAppWithCode: () => {},
|
||||
logInfo: () => {},
|
||||
logWarn: () => {},
|
||||
logError: () => {},
|
||||
},
|
||||
appReadyRuntimeDeps: {
|
||||
reloadConfigMainDeps: {
|
||||
reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
|
||||
logInfo: () => {},
|
||||
logWarning: () => {},
|
||||
showDesktopNotification: () => {},
|
||||
startConfigHotReload: () => {},
|
||||
refreshAnilistClientSecretState: async () => {},
|
||||
failHandlers: {
|
||||
logError: () => {},
|
||||
showErrorBox: () => {},
|
||||
quit: () => {},
|
||||
},
|
||||
},
|
||||
criticalConfigErrorMainDeps: {
|
||||
getConfigPath: () => '/tmp/config.jsonc',
|
||||
failHandlers: {
|
||||
logError: () => {},
|
||||
showErrorBox: () => {},
|
||||
quit: () => {},
|
||||
},
|
||||
},
|
||||
appReadyRuntimeMainDeps: {
|
||||
ensureDefaultConfigBootstrap: () => {},
|
||||
loadSubtitlePosition: () => {},
|
||||
resolveKeybindings: () => {},
|
||||
createMpvClient: () => {},
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => {},
|
||||
setLogLevel: () => {},
|
||||
initRuntimeOptionsManager: () => {},
|
||||
setSecondarySubMode: () => {},
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: 5174,
|
||||
defaultAnnotationWebsocketPort: 6678,
|
||||
defaultTexthookerPort: 5174,
|
||||
hasMpvWebsocketPlugin: () => false,
|
||||
startSubtitleWebsocket: () => {},
|
||||
startAnnotationWebsocket: () => {},
|
||||
startTexthooker: () => {},
|
||||
log: () => {},
|
||||
createMecabTokenizerAndCheck: async () => {},
|
||||
createSubtitleTimingTracker: () => {},
|
||||
loadYomitanExtension: async () => {},
|
||||
handleFirstRunSetup: async () => {},
|
||||
startJellyfinRemoteSession: async () => {},
|
||||
prewarmSubtitleDictionaries: async () => {},
|
||||
startBackgroundWarmups: () => {},
|
||||
texthookerOnlyMode: false,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||
setVisibleOverlayVisible: () => {},
|
||||
initializeOverlayRuntime: () => {},
|
||||
handleInitialArgs: () => {},
|
||||
logDebug: () => {},
|
||||
now: () => Date.now(),
|
||||
},
|
||||
immersionTrackerStartupMainDeps: {
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
getConfiguredDbPath: () => '/tmp/immersion.sqlite',
|
||||
createTrackerService: () =>
|
||||
({
|
||||
startSession: () => {},
|
||||
}) as never,
|
||||
setTracker: () => {},
|
||||
getMpvClient: () => null,
|
||||
seedTrackerFromCurrentMedia: () => {},
|
||||
logInfo: () => {},
|
||||
logDebug: () => {},
|
||||
logWarn: () => {},
|
||||
},
|
||||
},
|
||||
mpvRuntimeDeps: {
|
||||
bindMpvMainEventHandlersMainDeps: {
|
||||
appState: {
|
||||
initialArgs: null,
|
||||
overlayRuntimeInitialized: true,
|
||||
mpvClient: null,
|
||||
immersionTracker: null,
|
||||
subtitleTimingTracker: null,
|
||||
currentSubText: '',
|
||||
currentSubAssText: '',
|
||||
playbackPaused: null,
|
||||
previousSecondarySubVisibility: null,
|
||||
},
|
||||
getQuitOnDisconnectArmed: () => false,
|
||||
scheduleQuitCheck: () => {},
|
||||
quitApp: () => {},
|
||||
reportJellyfinRemoteStopped: () => {},
|
||||
syncOverlayMpvSubtitleSuppression: () => {},
|
||||
maybeRunAnilistPostWatchUpdate: async () => {},
|
||||
logSubtitleTimingError: () => {},
|
||||
broadcastToOverlayWindows: () => {},
|
||||
onSubtitleChange: () => {},
|
||||
refreshDiscordPresence: () => {},
|
||||
ensureImmersionTrackerInitialized: () => {},
|
||||
updateCurrentMediaPath: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
getCurrentAnilistMediaKey: () => null,
|
||||
resetAnilistMediaTracking: () => {},
|
||||
maybeProbeAnilistDuration: () => {},
|
||||
ensureAnilistMediaGuess: () => {},
|
||||
syncImmersionMediaState: () => {},
|
||||
updateCurrentMediaTitle: () => {},
|
||||
resetAnilistMediaGuessState: () => {},
|
||||
reportJellyfinRemoteProgress: () => {},
|
||||
updateSubtitleRenderMetrics: () => {},
|
||||
},
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: class FakeMpvClient {
|
||||
connected = true;
|
||||
on(): void {}
|
||||
connect(): void {}
|
||||
} as never,
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
||||
isAutoStartOverlayEnabled: () => true,
|
||||
setOverlayVisible: () => {},
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
},
|
||||
updateMpvSubtitleRenderMetricsMainDeps: {
|
||||
getCurrentMetrics: () => ({
|
||||
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,
|
||||
}),
|
||||
setCurrentMetrics: () => {},
|
||||
applyPatch: (current: any) => ({ next: current, changed: false }),
|
||||
broadcastMetrics: () => {},
|
||||
},
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: {
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => {},
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => {},
|
||||
getYomitanParserInitPromise: () => null,
|
||||
setYomitanParserInitPromise: () => {},
|
||||
isKnownWord: () => false,
|
||||
recordLookup: () => {},
|
||||
getKnownWordMatchMode: () => 'headword',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => null,
|
||||
getJlptEnabled: () => false,
|
||||
getFrequencyDictionaryEnabled: () => false,
|
||||
getFrequencyDictionaryMatchMode: () => 'headword',
|
||||
getFrequencyRank: () => null,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => null,
|
||||
},
|
||||
createTokenizerRuntimeDeps: () => ({}),
|
||||
tokenizeSubtitle: async () => ({ text: '' }),
|
||||
createMecabTokenizerAndCheckMainDeps: {
|
||||
getMecabTokenizer: () => null,
|
||||
setMecabTokenizer: () => {},
|
||||
createMecabTokenizer: () => ({}) as never,
|
||||
checkAvailability: async () => {},
|
||||
},
|
||||
prewarmSubtitleDictionariesMainDeps: {
|
||||
ensureJlptDictionaryLookup: async () => {},
|
||||
ensureFrequencyDictionaryLookup: async () => {},
|
||||
},
|
||||
},
|
||||
warmups: {
|
||||
launchBackgroundWarmupTaskMainDeps: {
|
||||
now: () => 0,
|
||||
logDebug: () => {},
|
||||
logWarn: () => {},
|
||||
},
|
||||
startBackgroundWarmupsMainDeps: {
|
||||
getStarted: () => false,
|
||||
setStarted: () => {},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
ensureYomitanExtensionLoaded: async () => {},
|
||||
shouldWarmupMecab: () => false,
|
||||
shouldWarmupYomitanExtension: () => false,
|
||||
shouldWarmupSubtitleDictionaries: () => false,
|
||||
shouldWarmupJellyfinRemoteSession: () => false,
|
||||
shouldAutoConnectJellyfinRemote: () => false,
|
||||
startJellyfinRemoteSession: async () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trayRuntimeDeps: {
|
||||
resolveTrayIconPathDeps: {} as never,
|
||||
buildTrayMenuTemplateDeps: {} as never,
|
||||
ensureTrayDeps: {} as never,
|
||||
destroyTrayDeps: {} as never,
|
||||
buildMenuFromTemplate: () => ({}) as never,
|
||||
},
|
||||
yomitanProfilePolicyDeps: {
|
||||
externalProfilePath: '',
|
||||
logInfo: () => {},
|
||||
},
|
||||
yomitanExtensionRuntimeDeps: {
|
||||
loadYomitanExtensionCore: async () => null,
|
||||
userDataPath: '/tmp',
|
||||
externalProfilePath: '',
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => {},
|
||||
setYomitanParserReadyPromise: () => {},
|
||||
setYomitanParserInitPromise: () => {},
|
||||
setYomitanExtension: () => {},
|
||||
setYomitanSession: () => {},
|
||||
getYomitanExtension: () => null,
|
||||
getLoadInFlight: () => null,
|
||||
setLoadInFlight: () => {},
|
||||
},
|
||||
yomitanSettingsRuntimeDeps: {
|
||||
ensureYomitanExtensionLoaded: async () => {},
|
||||
getYomitanSession: () => null,
|
||||
openYomitanSettingsWindow: () => {},
|
||||
getExistingWindow: () => null,
|
||||
setWindow: () => {},
|
||||
logWarn: () => {},
|
||||
logError: () => {},
|
||||
},
|
||||
createOverlayRuntimeBootstrapHandlers: () => ({
|
||||
initializeOverlayRuntime: () => {},
|
||||
}),
|
||||
initializeOverlayRuntimeMainDeps: {},
|
||||
initializeOverlayRuntimeBootstrapDeps: {},
|
||||
});
|
||||
|
||||
assert.equal(typeof runtimes.overlayVisibilityComposer.sendToActiveOverlayWindow, 'function');
|
||||
assert.equal(typeof runtimes.jellyfinRuntimeHandlers.runJellyfinCommand, 'function');
|
||||
assert.equal(typeof runtimes.anilistSetupHandlers.notifyAnilistSetup, 'function');
|
||||
assert.equal(typeof runtimes.openAnilistSetupWindow, 'function');
|
||||
assert.equal(typeof runtimes.anilistTrackingHandlers.maybeRunAnilistPostWatchUpdate, 'function');
|
||||
assert.equal(typeof runtimes.runStatsCliCommand, 'function');
|
||||
assert.equal(typeof runtimes.appReadyRuntime.appReadyRuntimeRunner, 'function');
|
||||
assert.equal(typeof runtimes.initializeOverlayRuntime, 'function');
|
||||
});
|
||||
127
src/main/boot/runtimes.ts
Normal file
127
src/main/boot/runtimes.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createOpenFirstRunSetupWindowHandler } from '../runtime/first-run-setup-window';
|
||||
import { createRunStatsCliCommandHandler } from '../runtime/stats-cli-command';
|
||||
import { createYomitanProfilePolicy } from '../runtime/yomitan-profile-policy';
|
||||
import {
|
||||
createBuildOpenAnilistSetupWindowMainDepsHandler,
|
||||
createMaybeFocusExistingAnilistSetupWindowHandler,
|
||||
createOpenAnilistSetupWindowHandler,
|
||||
} from '../runtime/domains/anilist';
|
||||
import {
|
||||
createTrayRuntimeHandlers,
|
||||
createYomitanExtensionRuntime,
|
||||
createYomitanSettingsRuntime,
|
||||
} from '../runtime/domains/overlay';
|
||||
import {
|
||||
composeAnilistSetupHandlers,
|
||||
composeAnilistTrackingHandlers,
|
||||
composeAppReadyRuntime,
|
||||
composeJellyfinRuntimeHandlers,
|
||||
composeMpvRuntimeHandlers,
|
||||
composeOverlayVisibilityRuntime,
|
||||
composeStatsStartupRuntime,
|
||||
} from '../runtime/composers';
|
||||
|
||||
export interface MainBootRuntimesParams<TBrowserWindow, TMpvClient, TTokenizerDeps, TSubtitleData> {
|
||||
overlayVisibilityRuntimeDeps: Parameters<typeof composeOverlayVisibilityRuntime>[0];
|
||||
jellyfinRuntimeHandlerDeps: Parameters<typeof composeJellyfinRuntimeHandlers>[0];
|
||||
anilistSetupDeps: Parameters<typeof composeAnilistSetupHandlers>[0];
|
||||
buildOpenAnilistSetupWindowMainDeps: Parameters<
|
||||
typeof createBuildOpenAnilistSetupWindowMainDepsHandler
|
||||
>[0];
|
||||
anilistTrackingDeps: Parameters<typeof composeAnilistTrackingHandlers>[0];
|
||||
statsStartupRuntimeDeps: Parameters<typeof composeStatsStartupRuntime>[0];
|
||||
runStatsCliCommandDeps: Parameters<typeof createRunStatsCliCommandHandler>[0];
|
||||
appReadyRuntimeDeps: Parameters<typeof composeAppReadyRuntime>[0];
|
||||
mpvRuntimeDeps: any;
|
||||
trayRuntimeDeps: Parameters<typeof createTrayRuntimeHandlers>[0];
|
||||
yomitanProfilePolicyDeps: Parameters<typeof createYomitanProfilePolicy>[0];
|
||||
yomitanExtensionRuntimeDeps: Parameters<typeof createYomitanExtensionRuntime>[0];
|
||||
yomitanSettingsRuntimeDeps: Parameters<typeof createYomitanSettingsRuntime>[0];
|
||||
createOverlayRuntimeBootstrapHandlers: (params: {
|
||||
initializeOverlayRuntimeMainDeps: unknown;
|
||||
initializeOverlayRuntimeBootstrapDeps: unknown;
|
||||
}) => {
|
||||
initializeOverlayRuntime: () => void;
|
||||
};
|
||||
initializeOverlayRuntimeMainDeps: unknown;
|
||||
initializeOverlayRuntimeBootstrapDeps: unknown;
|
||||
}
|
||||
|
||||
export function createMainBootRuntimes<
|
||||
TBrowserWindow,
|
||||
TMpvClient,
|
||||
TTokenizerDeps,
|
||||
TSubtitleData,
|
||||
>(
|
||||
params: MainBootRuntimesParams<TBrowserWindow, TMpvClient, TTokenizerDeps, TSubtitleData>,
|
||||
) {
|
||||
const overlayVisibilityComposer = composeOverlayVisibilityRuntime(
|
||||
params.overlayVisibilityRuntimeDeps,
|
||||
);
|
||||
const jellyfinRuntimeHandlers = composeJellyfinRuntimeHandlers(
|
||||
params.jellyfinRuntimeHandlerDeps,
|
||||
);
|
||||
const anilistSetupHandlers = composeAnilistSetupHandlers(params.anilistSetupDeps);
|
||||
const buildOpenAnilistSetupWindowMainDepsHandler =
|
||||
createBuildOpenAnilistSetupWindowMainDepsHandler(params.buildOpenAnilistSetupWindowMainDeps);
|
||||
const maybeFocusExistingAnilistSetupWindow =
|
||||
params.buildOpenAnilistSetupWindowMainDeps.maybeFocusExistingSetupWindow;
|
||||
const anilistTrackingHandlers = composeAnilistTrackingHandlers(params.anilistTrackingDeps);
|
||||
const statsStartupRuntime = composeStatsStartupRuntime(params.statsStartupRuntimeDeps);
|
||||
const runStatsCliCommand = createRunStatsCliCommandHandler(params.runStatsCliCommandDeps);
|
||||
const appReadyRuntime = composeAppReadyRuntime(params.appReadyRuntimeDeps);
|
||||
const mpvRuntimeHandlers = composeMpvRuntimeHandlers<any, any, any>(
|
||||
params.mpvRuntimeDeps as any,
|
||||
);
|
||||
const trayRuntimeHandlers = createTrayRuntimeHandlers(params.trayRuntimeDeps);
|
||||
const yomitanProfilePolicy = createYomitanProfilePolicy(params.yomitanProfilePolicyDeps);
|
||||
const yomitanExtensionRuntime = createYomitanExtensionRuntime(
|
||||
params.yomitanExtensionRuntimeDeps,
|
||||
);
|
||||
const yomitanSettingsRuntime = createYomitanSettingsRuntime(
|
||||
params.yomitanSettingsRuntimeDeps,
|
||||
);
|
||||
const overlayRuntimeBootstrapHandlers = params.createOverlayRuntimeBootstrapHandlers({
|
||||
initializeOverlayRuntimeMainDeps: params.initializeOverlayRuntimeMainDeps,
|
||||
initializeOverlayRuntimeBootstrapDeps: params.initializeOverlayRuntimeBootstrapDeps,
|
||||
});
|
||||
|
||||
return {
|
||||
overlayVisibilityComposer,
|
||||
jellyfinRuntimeHandlers,
|
||||
anilistSetupHandlers,
|
||||
maybeFocusExistingAnilistSetupWindow,
|
||||
buildOpenAnilistSetupWindowMainDepsHandler,
|
||||
openAnilistSetupWindow: () =>
|
||||
createOpenAnilistSetupWindowHandler(buildOpenAnilistSetupWindowMainDepsHandler())(),
|
||||
anilistTrackingHandlers,
|
||||
statsStartupRuntime,
|
||||
runStatsCliCommand,
|
||||
appReadyRuntime,
|
||||
mpvRuntimeHandlers,
|
||||
trayRuntimeHandlers,
|
||||
yomitanProfilePolicy,
|
||||
yomitanExtensionRuntime,
|
||||
yomitanSettingsRuntime,
|
||||
initializeOverlayRuntime: overlayRuntimeBootstrapHandlers.initializeOverlayRuntime,
|
||||
openFirstRunSetupWindowHandler: createOpenFirstRunSetupWindowHandler,
|
||||
};
|
||||
}
|
||||
|
||||
export const composeBootOverlayVisibilityRuntime = composeOverlayVisibilityRuntime;
|
||||
export const composeBootJellyfinRuntimeHandlers = composeJellyfinRuntimeHandlers;
|
||||
export const composeBootAnilistSetupHandlers = composeAnilistSetupHandlers;
|
||||
export const composeBootAnilistTrackingHandlers = composeAnilistTrackingHandlers;
|
||||
export const composeBootStatsStartupRuntime = composeStatsStartupRuntime;
|
||||
export const createBootRunStatsCliCommandHandler = createRunStatsCliCommandHandler;
|
||||
export const composeBootAppReadyRuntime = composeAppReadyRuntime;
|
||||
export const composeBootMpvRuntimeHandlers = composeMpvRuntimeHandlers;
|
||||
export const createBootTrayRuntimeHandlers = createTrayRuntimeHandlers;
|
||||
export const createBootYomitanProfilePolicy = createYomitanProfilePolicy;
|
||||
export const createBootYomitanExtensionRuntime = createYomitanExtensionRuntime;
|
||||
export const createBootYomitanSettingsRuntime = createYomitanSettingsRuntime;
|
||||
export const createBootMaybeFocusExistingAnilistSetupWindowHandler =
|
||||
createMaybeFocusExistingAnilistSetupWindowHandler;
|
||||
export const createBootBuildOpenAnilistSetupWindowMainDepsHandler =
|
||||
createBuildOpenAnilistSetupWindowMainDepsHandler;
|
||||
export const createBootOpenAnilistSetupWindowHandler = createOpenAnilistSetupWindowHandler;
|
||||
83
src/main/boot/services.test.ts
Normal file
83
src/main/boot/services.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createMainBootServices } from './services';
|
||||
|
||||
test('createMainBootServices builds boot-phase service bundle', () => {
|
||||
const calls: string[] = [];
|
||||
let setPathValue: string | null = null;
|
||||
|
||||
const services = createMainBootServices({
|
||||
platform: 'linux',
|
||||
argv: ['node', 'main.ts'],
|
||||
appDataDir: undefined,
|
||||
xdgConfigHome: undefined,
|
||||
homeDir: '/home/tester',
|
||||
defaultMpvLogFile: '/tmp/default.log',
|
||||
envMpvLog: ' /tmp/custom.log ',
|
||||
defaultTexthookerPort: 5174,
|
||||
getDefaultSocketPath: () => '/tmp/subminer.sock',
|
||||
resolveConfigDir: () => '/tmp/subminer-config',
|
||||
existsSync: () => false,
|
||||
mkdirSync: (targetPath) => {
|
||||
calls.push(`mkdir:${targetPath}`);
|
||||
},
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
app: {
|
||||
setPath: (_name, value) => {
|
||||
setPathValue = value;
|
||||
},
|
||||
quit: () => {},
|
||||
on: () => ({}),
|
||||
whenReady: async () => {},
|
||||
},
|
||||
shouldBypassSingleInstanceLock: () => false,
|
||||
requestSingleInstanceLockEarly: () => true,
|
||||
registerSecondInstanceHandlerEarly: () => {},
|
||||
onConfigStartupParseError: () => {
|
||||
throw new Error('unexpected parse failure');
|
||||
},
|
||||
createConfigService: (configDir) => ({ configDir }),
|
||||
createAnilistTokenStore: (targetPath) => ({ targetPath }),
|
||||
createJellyfinTokenStore: (targetPath) => ({ targetPath }),
|
||||
createAnilistUpdateQueue: (targetPath) => ({ targetPath }),
|
||||
createSubtitleWebSocket: () => ({ kind: 'ws' }),
|
||||
createLogger: (scope) =>
|
||||
({
|
||||
scope,
|
||||
warn: () => {},
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
}) as const,
|
||||
createMainRuntimeRegistry: () => ({ registry: true }),
|
||||
createOverlayManager: () => ({
|
||||
getModalWindow: () => null,
|
||||
}),
|
||||
createOverlayModalInputState: () => ({ inputState: true }),
|
||||
createOverlayContentMeasurementStore: () => ({ measurementStore: true }),
|
||||
getSyncOverlayShortcutsForModal: () => () => {},
|
||||
getSyncOverlayVisibilityForModal: () => () => {},
|
||||
createOverlayModalRuntime: () => ({ modalRuntime: true }),
|
||||
createAppState: (input) => ({ ...input }),
|
||||
});
|
||||
|
||||
assert.equal(services.configDir, '/tmp/subminer-config');
|
||||
assert.equal(services.userDataPath, '/tmp/subminer-config');
|
||||
assert.equal(services.defaultMpvLogPath, '/tmp/custom.log');
|
||||
assert.equal(services.defaultImmersionDbPath, '/tmp/subminer-config/immersion.sqlite');
|
||||
assert.deepEqual(services.configService, { configDir: '/tmp/subminer-config' });
|
||||
assert.deepEqual(services.anilistTokenStore, {
|
||||
targetPath: '/tmp/subminer-config/anilist-token-store.json',
|
||||
});
|
||||
assert.deepEqual(services.jellyfinTokenStore, {
|
||||
targetPath: '/tmp/subminer-config/jellyfin-token-store.json',
|
||||
});
|
||||
assert.deepEqual(services.anilistUpdateQueue, {
|
||||
targetPath: '/tmp/subminer-config/anilist-retry-queue.json',
|
||||
});
|
||||
assert.deepEqual(services.appState, {
|
||||
mpvSocketPath: '/tmp/subminer.sock',
|
||||
texthookerPort: 5174,
|
||||
});
|
||||
assert.deepEqual(calls, ['mkdir:/tmp/subminer-config']);
|
||||
assert.equal(setPathValue, '/tmp/subminer-config');
|
||||
});
|
||||
262
src/main/boot/services.ts
Normal file
262
src/main/boot/services.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { ConfigStartupParseError } from '../../config';
|
||||
|
||||
export interface MainBootServicesParams<
|
||||
TConfigService,
|
||||
TAnilistTokenStore,
|
||||
TJellyfinTokenStore,
|
||||
TAnilistUpdateQueue,
|
||||
TSubtitleWebSocket,
|
||||
TLogger,
|
||||
TRuntimeRegistry,
|
||||
TOverlayManager,
|
||||
TOverlayModalInputState,
|
||||
TOverlayContentMeasurementStore,
|
||||
TOverlayModalRuntime,
|
||||
TAppState,
|
||||
TAppLifecycleApp,
|
||||
> {
|
||||
platform: NodeJS.Platform;
|
||||
argv: string[];
|
||||
appDataDir: string | undefined;
|
||||
xdgConfigHome: string | undefined;
|
||||
homeDir: string;
|
||||
defaultMpvLogFile: string;
|
||||
envMpvLog: string | undefined;
|
||||
defaultTexthookerPort: number;
|
||||
getDefaultSocketPath: () => string;
|
||||
resolveConfigDir: (input: {
|
||||
platform: NodeJS.Platform;
|
||||
appDataDir: string | undefined;
|
||||
xdgConfigHome: string | undefined;
|
||||
homeDir: string;
|
||||
existsSync: (targetPath: string) => boolean;
|
||||
}) => string;
|
||||
existsSync: (targetPath: string) => boolean;
|
||||
mkdirSync: (targetPath: string, options: { recursive: true }) => void;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
app: {
|
||||
setPath: (name: string, value: string) => void;
|
||||
quit: () => void;
|
||||
on: (...args: any[]) => unknown;
|
||||
whenReady: () => Promise<void>;
|
||||
};
|
||||
shouldBypassSingleInstanceLock: () => boolean;
|
||||
requestSingleInstanceLockEarly: () => boolean;
|
||||
registerSecondInstanceHandlerEarly: (
|
||||
listener: (_event: unknown, argv: string[]) => void,
|
||||
) => void;
|
||||
onConfigStartupParseError: (error: ConfigStartupParseError) => void;
|
||||
createConfigService: (configDir: string) => TConfigService;
|
||||
createAnilistTokenStore: (targetPath: string) => TAnilistTokenStore;
|
||||
createJellyfinTokenStore: (targetPath: string) => TJellyfinTokenStore;
|
||||
createAnilistUpdateQueue: (targetPath: string) => TAnilistUpdateQueue;
|
||||
createSubtitleWebSocket: () => TSubtitleWebSocket;
|
||||
createLogger: (scope: string) => TLogger & {
|
||||
warn: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
error: (message: string, details?: unknown) => void;
|
||||
};
|
||||
createMainRuntimeRegistry: () => TRuntimeRegistry;
|
||||
createOverlayManager: () => TOverlayManager;
|
||||
createOverlayModalInputState: (params: any) => TOverlayModalInputState;
|
||||
createOverlayContentMeasurementStore: (params: {
|
||||
logger: TLogger;
|
||||
}) => TOverlayContentMeasurementStore;
|
||||
getSyncOverlayShortcutsForModal: () => (isActive: boolean) => void;
|
||||
getSyncOverlayVisibilityForModal: () => () => void;
|
||||
createOverlayModalRuntime: (params: {
|
||||
overlayManager: TOverlayManager;
|
||||
overlayModalInputState: TOverlayModalInputState;
|
||||
onModalStateChange: (isActive: boolean) => void;
|
||||
}) => TOverlayModalRuntime;
|
||||
createAppState: (input: {
|
||||
mpvSocketPath: string;
|
||||
texthookerPort: number;
|
||||
}) => TAppState;
|
||||
}
|
||||
|
||||
export interface MainBootServicesResult<
|
||||
TConfigService,
|
||||
TAnilistTokenStore,
|
||||
TJellyfinTokenStore,
|
||||
TAnilistUpdateQueue,
|
||||
TSubtitleWebSocket,
|
||||
TLogger,
|
||||
TRuntimeRegistry,
|
||||
TOverlayManager,
|
||||
TOverlayModalInputState,
|
||||
TOverlayContentMeasurementStore,
|
||||
TOverlayModalRuntime,
|
||||
TAppState,
|
||||
TAppLifecycleApp,
|
||||
> {
|
||||
configDir: string;
|
||||
userDataPath: string;
|
||||
defaultMpvLogPath: string;
|
||||
defaultImmersionDbPath: string;
|
||||
configService: TConfigService;
|
||||
anilistTokenStore: TAnilistTokenStore;
|
||||
jellyfinTokenStore: TJellyfinTokenStore;
|
||||
anilistUpdateQueue: TAnilistUpdateQueue;
|
||||
subtitleWsService: TSubtitleWebSocket;
|
||||
annotationSubtitleWsService: TSubtitleWebSocket;
|
||||
logger: TLogger;
|
||||
runtimeRegistry: TRuntimeRegistry;
|
||||
overlayManager: TOverlayManager;
|
||||
overlayModalInputState: TOverlayModalInputState;
|
||||
overlayContentMeasurementStore: TOverlayContentMeasurementStore;
|
||||
overlayModalRuntime: TOverlayModalRuntime;
|
||||
appState: TAppState;
|
||||
appLifecycleApp: TAppLifecycleApp;
|
||||
}
|
||||
|
||||
export function createMainBootServices<
|
||||
TConfigService,
|
||||
TAnilistTokenStore,
|
||||
TJellyfinTokenStore,
|
||||
TAnilistUpdateQueue,
|
||||
TSubtitleWebSocket,
|
||||
TLogger,
|
||||
TRuntimeRegistry,
|
||||
TOverlayManager extends { getModalWindow: () => unknown },
|
||||
TOverlayModalInputState,
|
||||
TOverlayContentMeasurementStore,
|
||||
TOverlayModalRuntime,
|
||||
TAppState,
|
||||
TAppLifecycleApp,
|
||||
>(
|
||||
params: MainBootServicesParams<
|
||||
TConfigService,
|
||||
TAnilistTokenStore,
|
||||
TJellyfinTokenStore,
|
||||
TAnilistUpdateQueue,
|
||||
TSubtitleWebSocket,
|
||||
TLogger,
|
||||
TRuntimeRegistry,
|
||||
TOverlayManager,
|
||||
TOverlayModalInputState,
|
||||
TOverlayContentMeasurementStore,
|
||||
TOverlayModalRuntime,
|
||||
TAppState,
|
||||
TAppLifecycleApp
|
||||
>,
|
||||
): MainBootServicesResult<
|
||||
TConfigService,
|
||||
TAnilistTokenStore,
|
||||
TJellyfinTokenStore,
|
||||
TAnilistUpdateQueue,
|
||||
TSubtitleWebSocket,
|
||||
TLogger,
|
||||
TRuntimeRegistry,
|
||||
TOverlayManager,
|
||||
TOverlayModalInputState,
|
||||
TOverlayContentMeasurementStore,
|
||||
TOverlayModalRuntime,
|
||||
TAppState,
|
||||
TAppLifecycleApp
|
||||
> {
|
||||
const configDir = params.resolveConfigDir({
|
||||
platform: params.platform,
|
||||
appDataDir: params.appDataDir,
|
||||
xdgConfigHome: params.xdgConfigHome,
|
||||
homeDir: params.homeDir,
|
||||
existsSync: params.existsSync,
|
||||
});
|
||||
const userDataPath = configDir;
|
||||
const defaultMpvLogPath = params.envMpvLog?.trim() || params.defaultMpvLogFile;
|
||||
const defaultImmersionDbPath = params.joinPath(userDataPath, 'immersion.sqlite');
|
||||
|
||||
const configService = (() => {
|
||||
try {
|
||||
return params.createConfigService(configDir);
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigStartupParseError) {
|
||||
params.onConfigStartupParseError(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
const anilistTokenStore = params.createAnilistTokenStore(
|
||||
params.joinPath(userDataPath, 'anilist-token-store.json'),
|
||||
);
|
||||
const jellyfinTokenStore = params.createJellyfinTokenStore(
|
||||
params.joinPath(userDataPath, 'jellyfin-token-store.json'),
|
||||
);
|
||||
const anilistUpdateQueue = params.createAnilistUpdateQueue(
|
||||
params.joinPath(userDataPath, 'anilist-retry-queue.json'),
|
||||
);
|
||||
const subtitleWsService = params.createSubtitleWebSocket();
|
||||
const annotationSubtitleWsService = params.createSubtitleWebSocket();
|
||||
const logger = params.createLogger('main');
|
||||
const runtimeRegistry = params.createMainRuntimeRegistry();
|
||||
const overlayManager = params.createOverlayManager();
|
||||
const overlayModalInputState = params.createOverlayModalInputState({
|
||||
getModalWindow: () => overlayManager.getModalWindow(),
|
||||
syncOverlayShortcutsForModal: (isActive: boolean) => {
|
||||
params.getSyncOverlayShortcutsForModal()(isActive);
|
||||
},
|
||||
syncOverlayVisibilityForModal: () => {
|
||||
params.getSyncOverlayVisibilityForModal()();
|
||||
},
|
||||
});
|
||||
const overlayContentMeasurementStore = params.createOverlayContentMeasurementStore({
|
||||
logger,
|
||||
});
|
||||
const overlayModalRuntime = params.createOverlayModalRuntime({
|
||||
overlayManager,
|
||||
overlayModalInputState,
|
||||
onModalStateChange: (isActive: boolean) =>
|
||||
(overlayModalInputState as { handleModalInputStateChange?: (isActive: boolean) => void })
|
||||
.handleModalInputStateChange?.(isActive),
|
||||
});
|
||||
const appState = params.createAppState({
|
||||
mpvSocketPath: params.getDefaultSocketPath(),
|
||||
texthookerPort: params.defaultTexthookerPort,
|
||||
});
|
||||
|
||||
if (!params.existsSync(userDataPath)) {
|
||||
params.mkdirSync(userDataPath, { recursive: true });
|
||||
}
|
||||
params.app.setPath('userData', userDataPath);
|
||||
|
||||
const appLifecycleApp = {
|
||||
requestSingleInstanceLock: () =>
|
||||
params.shouldBypassSingleInstanceLock()
|
||||
? true
|
||||
: params.requestSingleInstanceLockEarly(),
|
||||
quit: () => params.app.quit(),
|
||||
on: (event: string, listener: (...args: unknown[]) => void) => {
|
||||
if (event === 'second-instance') {
|
||||
params.registerSecondInstanceHandlerEarly(
|
||||
listener as (_event: unknown, argv: string[]) => void,
|
||||
);
|
||||
return params.app;
|
||||
}
|
||||
params.app.on(event, listener);
|
||||
return params.app;
|
||||
},
|
||||
whenReady: () => params.app.whenReady(),
|
||||
} as TAppLifecycleApp;
|
||||
|
||||
return {
|
||||
configDir,
|
||||
userDataPath,
|
||||
defaultMpvLogPath,
|
||||
defaultImmersionDbPath,
|
||||
configService,
|
||||
anilistTokenStore,
|
||||
jellyfinTokenStore,
|
||||
anilistUpdateQueue,
|
||||
subtitleWsService,
|
||||
annotationSubtitleWsService,
|
||||
logger,
|
||||
runtimeRegistry,
|
||||
overlayManager,
|
||||
overlayModalInputState,
|
||||
overlayContentMeasurementStore,
|
||||
overlayModalRuntime,
|
||||
appState,
|
||||
appLifecycleApp,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user