mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
refactor(main): move domains into per-domain assemblies (#309)
* refactor(main): move the YouTube domain into an assembly createYoutubeAssembly owns the YouTube flow, playback, media cache, whisper subtitles, primary-subtitle notifications, and the in-app browser, plus the browser quit-on-close state. main.ts builds it once where the first YouTube runtime used to be and calls through the result. Siblings built later are passed as getters or callbacks; tests pin that construction calls none of them. * docs(architecture): describe per-domain assemblies * test(e2e): blank the line before mining the sub-delay cue The previous test leaves the same cue on screen, so the overlay check passed before the seek landed and --mine-sentence could read mpv's empty mid-seek sub-text and skip the card. * refactor(main): move the Jellyfin domain into an assembly createJellyfinAssembly owns the Jellyfin composer call, its playback and remote-progress state, the subtitle cache IO, mpv defaults, and the tray discovery deps. main.ts builds it where the composer call used to be and reads state through getActivePlayback/getQuitOnDisconnectArmed. * refactor(main): move the AniList domain into an assembly createAnilistAssembly owns the AniList setup and tracking composer calls, the setup window, the state runtime, and the token, media-guess, in-flight and attempted-update state they share. main.ts builds it where the setup composer call used to be and keeps the rate limiter, which the stats cover-art fetcher also uses. * refactor(main): move config hot-reload, settings, and sync UI into assemblies createSettingsAssembly owns config hot-reload and the settings window, which share the hot-reload-applied handler; main.ts still supplies that handler's cross-domain deps and the Yomitan deck-name helper. createSyncAssembly owns the sync window, sync IPC runtime, and auto-sync scheduler. Both are built where their blocks were; main.ts keeps the registerHandlers()/start() calls at the same points. * refactor(main): move dictionary runtimes into assemblies createDictionariesAssembly owns the JLPT and frequency lookups and their dictionary-root builders; main.ts still resolves the root paths at the same point. createCharacterDictionaryAssembly owns the character dictionary runtime, Yomitan auto-sync, and the portrait and name-candidate lookups. Both are built where their blocks were; later siblings (Yomitan profile policy, notifications, OSD, subtitle refresh) come in as getters or callbacks. * refactor(main): move overlay modal openers into an assembly createOverlayModalsAssembly owns the shared open-with-notification helper and the runtime options, Jimaku, TsukiHime, subtitle generation, session help, changelog, character dictionary manager, controller, and playlist browser openers. main.ts builds it where the openers used to live. * refactor(logger): export the Logger type for assembly deps The build emits declarations, and the Jellyfin assembly returns the logger in its tray discovery deps, so tsc needs a nameable Logger type. Assemblies now Pick from it directly.
This commit is contained in:
1 parent
a9a4c4f083
commit
2924f04316
23 files changed
+3173
-2010
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import type { BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
|
||||
import type { ConfigService } from '../../config/service';
|
||||
import { promoteSettingsWindowAboveOverlay } from '../../core/services';
|
||||
import type { Logger } from '../../logger';
|
||||
import { getSyncHostsPath } from '../../shared/sync/sync-hosts-store';
|
||||
import { createOpenConfigSettingsWindowHandler } from '../runtime/config-settings-window';
|
||||
import type { createOverlayNotificationsRuntime } from '../runtime/overlay-notifications-runtime';
|
||||
import { createCreateSyncUiWindowHandler } from '../runtime/setup-window-factory';
|
||||
import { createSyncAutoScheduler } from '../runtime/sync-auto-scheduler';
|
||||
import { resolveSyncLauncherCommand, runSyncLauncher } from '../runtime/sync-launcher-client';
|
||||
import { createSyncUiRuntime, type SyncUiRuntimeDeps } from '../runtime/sync-ui-runtime';
|
||||
import type { AppState } from '../state';
|
||||
|
||||
/**
|
||||
* What the sync window and auto-sync scheduler need from the rest of the app. Siblings built
|
||||
* after this assembly in `main.ts` are reached through getters or callbacks.
|
||||
*/
|
||||
export interface SyncAssemblyDeps {
|
||||
appState: Pick<AppState, 'syncUiWindow' | 'initialArgs'>;
|
||||
configService: Pick<ConfigService, 'getConfig'>;
|
||||
logger: Pick<Logger, 'info' | 'error'>;
|
||||
userDataPath: string;
|
||||
defaultImmersionDbPath: string;
|
||||
getOverlayNotifications: () => Pick<
|
||||
ReturnType<typeof createOverlayNotificationsRuntime>,
|
||||
'showOverlayNotification'
|
||||
>;
|
||||
requestAppQuit: () => void;
|
||||
createBrowserWindow: (options: BrowserWindowConstructorOptions) => BrowserWindow;
|
||||
/** Directory of the main bundle; the sync preload and page live next to it. */
|
||||
mainDirname: string;
|
||||
ipcMain: SyncUiRuntimeDeps['ipcMain'];
|
||||
/** Electron app details the launcher command depends on. */
|
||||
isPackaged: () => boolean;
|
||||
getAppPath: () => string;
|
||||
pickSnapshotFile: SyncUiRuntimeDeps['pickSnapshotFile'];
|
||||
revealPath: SyncUiRuntimeDeps['revealPath'];
|
||||
}
|
||||
|
||||
export type SyncAssembly = ReturnType<typeof createSyncAssembly>;
|
||||
|
||||
/**
|
||||
* Owns the stats sync window, the sync IPC runtime, and the auto-sync scheduler. `main.ts`
|
||||
* registers the IPC handlers, starts the scheduler, and stops both on quit.
|
||||
*/
|
||||
export function createSyncAssembly(deps: SyncAssemblyDeps) {
|
||||
const { appState, configService, logger } = deps;
|
||||
|
||||
const openSyncUiWindowHandler = createOpenConfigSettingsWindowHandler({
|
||||
getSettingsWindow: () => appState.syncUiWindow,
|
||||
setSettingsWindow: (window) => {
|
||||
appState.syncUiWindow = window as BrowserWindow | null;
|
||||
},
|
||||
createSettingsWindow: createCreateSyncUiWindowHandler({
|
||||
createBrowserWindow: (options) => deps.createBrowserWindow(options),
|
||||
preloadPath: path.join(deps.mainDirname, 'preload-syncui.js'),
|
||||
}),
|
||||
settingsHtmlPath: path.join(deps.mainDirname, 'syncui', 'index.html'),
|
||||
promoteSettingsWindowAboveOverlay: (window) =>
|
||||
promoteSettingsWindowAboveOverlay(window as BrowserWindow),
|
||||
onClosed: () => {
|
||||
// requestAppQuit (not app.quit) so the forced-exit fallback covers a
|
||||
// stalled quit; a standalone sync window is the app's only reason to live.
|
||||
if (appState.initialArgs?.syncWindow) deps.requestAppQuit();
|
||||
},
|
||||
log: (message) => logger.error(message),
|
||||
});
|
||||
|
||||
const syncUiRuntime = createSyncUiRuntime({
|
||||
ipcMain: deps.ipcMain,
|
||||
hostsFilePath: getSyncHostsPath(deps.userDataPath),
|
||||
snapshotsDir: path.join(os.tmpdir(), 'subminer-db-snapshots'),
|
||||
getDbPath: () => {
|
||||
const configured = configService.getConfig().immersionTracking?.dbPath?.trim();
|
||||
return configured || deps.defaultImmersionDbPath;
|
||||
},
|
||||
resolveLauncherCommand: () =>
|
||||
resolveSyncLauncherCommand({
|
||||
execPath: process.execPath,
|
||||
appPath: deps.isPackaged() ? null : deps.getAppPath(),
|
||||
}),
|
||||
runLauncher: runSyncLauncher,
|
||||
getWindow: () => appState.syncUiWindow,
|
||||
pickSnapshotFile: deps.pickSnapshotFile,
|
||||
revealPath: deps.revealPath,
|
||||
nowMs: () => Date.now(),
|
||||
log: (message) => logger.info(message),
|
||||
notify: (payload) =>
|
||||
deps.getOverlayNotifications().showOverlayNotification({
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
variant: payload.variant,
|
||||
}),
|
||||
});
|
||||
|
||||
// Live sync uses a consistent WAL snapshot plus a transactional merge, and it
|
||||
// skips unfinished sessions, so resident stats writers do not block scheduling.
|
||||
const syncAutoScheduler = createSyncAutoScheduler({
|
||||
readState: () => syncUiRuntime.readState(),
|
||||
isRunning: () => syncUiRuntime.isRunning(),
|
||||
triggerHostSync: (host, direction) => {
|
||||
void syncUiRuntime.runHostSync({ host, direction }, { notify: true });
|
||||
},
|
||||
nowMs: () => Date.now(),
|
||||
log: (message) => logger.info(message),
|
||||
});
|
||||
|
||||
return {
|
||||
openWindow: openSyncUiWindowHandler,
|
||||
runtime: syncUiRuntime,
|
||||
autoScheduler: syncAutoScheduler,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user