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,141 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
|
||||
import { AnkiConnectClient } from '../../anki-connect';
|
||||
import { DEFAULT_CONFIG } from '../../config/definitions';
|
||||
import type { ConfigService } from '../../config/service';
|
||||
import { buildConfigSettingsRegistry } from '../../config/settings/registry';
|
||||
import {
|
||||
createConfigHotReloadRuntime,
|
||||
promoteSettingsWindowAboveOverlay,
|
||||
} from '../../core/services';
|
||||
import type { Logger } from '../../logger';
|
||||
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||
import {
|
||||
buildConfigWarningDialogDetails,
|
||||
buildConfigWarningNotificationBody,
|
||||
} from '../config-validation';
|
||||
import {
|
||||
buildRestartRequiredConfigMessage,
|
||||
createConfigHotReloadAppliedHandler,
|
||||
createConfigHotReloadMessageHandler,
|
||||
createWatchConfigPathHandler,
|
||||
} from '../runtime/config-hot-reload-handlers';
|
||||
import {
|
||||
createConfigSettingsRuntime,
|
||||
type ConfigSettingsIpcMainLike,
|
||||
} from '../runtime/config-settings-runtime';
|
||||
import type { createOverlayNotificationsRuntime } from '../runtime/overlay-notifications-runtime';
|
||||
import { createCreateConfigSettingsWindowHandler } from '../runtime/setup-window-factory';
|
||||
import type { AppState } from '../state';
|
||||
|
||||
type ConfigHotReloadMessageDeps = Parameters<typeof createConfigHotReloadMessageHandler>[0];
|
||||
|
||||
/**
|
||||
* What config hot-reload and the settings window need from the rest of the app. Siblings built
|
||||
* after this assembly in `main.ts` are reached through getters or callbacks.
|
||||
*/
|
||||
export interface SettingsAssemblyDeps {
|
||||
appState: Pick<AppState, 'configSettingsWindow'>;
|
||||
configService: Pick<
|
||||
ConfigService,
|
||||
'getConfig' | 'getConfigPath' | 'getRawConfig' | 'getWarnings' | 'reloadConfigStrict'
|
||||
>;
|
||||
logger: Pick<Logger, 'error'>;
|
||||
getOverlayNotifications: () => Pick<
|
||||
ReturnType<typeof createOverlayNotificationsRuntime>,
|
||||
'getConfiguredStatusNotificationType' | 'showOverlayNotification'
|
||||
>;
|
||||
showMpvOsd: (message: string) => void;
|
||||
showDesktopNotification: ConfigHotReloadMessageDeps['showDesktopNotification'];
|
||||
/** Applies a reloaded config to the running app; shared by file hot-reload and the settings window. */
|
||||
hotReloadApplied: Parameters<typeof createConfigHotReloadAppliedHandler>[0];
|
||||
getYomitanAnkiDeckName: () => Promise<string>;
|
||||
showErrorBox: (title: string, content: string) => void;
|
||||
createBrowserWindow: (options: BrowserWindowConstructorOptions) => BrowserWindow;
|
||||
/** Directory of the main bundle; the settings preload and page live next to it. */
|
||||
mainDirname: string;
|
||||
openPath: (targetPath: string) => Promise<string>;
|
||||
ipcMain: ConfigSettingsIpcMainLike;
|
||||
}
|
||||
|
||||
export type SettingsAssembly = ReturnType<typeof createSettingsAssembly>;
|
||||
|
||||
/**
|
||||
* Owns config file hot-reload and the settings window, which both apply reloaded config through
|
||||
* the same handler. `main.ts` starts/stops hot-reload and registers the settings IPC handlers.
|
||||
*/
|
||||
export function createSettingsAssembly(deps: SettingsAssemblyDeps) {
|
||||
const { appState, configService, logger } = deps;
|
||||
const configSettingsFields = buildConfigSettingsRegistry(DEFAULT_CONFIG);
|
||||
|
||||
const notifyConfigHotReloadMessage = createConfigHotReloadMessageHandler({
|
||||
getNotificationType: () => deps.getOverlayNotifications().getConfiguredStatusNotificationType(),
|
||||
showMpvOsd: (message) => deps.showMpvOsd(message),
|
||||
showOverlayNotification: (payload) =>
|
||||
deps.getOverlayNotifications().showOverlayNotification(payload),
|
||||
showDesktopNotification: (title, options) => deps.showDesktopNotification(title, options),
|
||||
});
|
||||
const watchConfigPathHandler = createWatchConfigPathHandler({
|
||||
fileExists: (targetPath) => fs.existsSync(targetPath),
|
||||
dirname: (targetPath) => path.dirname(targetPath),
|
||||
watchPath: (targetPath, listener) => fs.watch(targetPath, listener),
|
||||
});
|
||||
const applyConfigHotReloadDiff = createConfigHotReloadAppliedHandler(deps.hotReloadApplied);
|
||||
const configHotReloadRuntime = createConfigHotReloadRuntime({
|
||||
getCurrentConfig: () => configService.getConfig(),
|
||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||
watchConfigPath: (configPath, onChange) => watchConfigPathHandler(configPath, onChange),
|
||||
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
||||
clearTimeout: (timeout) => clearTimeout(timeout),
|
||||
debounceMs: 250,
|
||||
onHotReloadApplied: applyConfigHotReloadDiff,
|
||||
onRestartRequired: (fields) =>
|
||||
notifyConfigHotReloadMessage(buildRestartRequiredConfigMessage(fields)),
|
||||
onInvalidConfig: notifyConfigHotReloadMessage,
|
||||
onValidationWarnings: (configPath, warnings) => {
|
||||
deps.showDesktopNotification('SubMiner', {
|
||||
body: buildConfigWarningNotificationBody(configPath, warnings),
|
||||
});
|
||||
if (process.platform === 'darwin') {
|
||||
deps.showErrorBox(
|
||||
'SubMiner config validation warning',
|
||||
buildConfigWarningDialogDetails(configPath, warnings),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const configSettingsRuntime = createConfigSettingsRuntime({
|
||||
fields: configSettingsFields,
|
||||
getConfigPath: () => configService.getConfigPath(),
|
||||
getRawConfig: () => configService.getRawConfig(),
|
||||
getConfig: () => configService.getConfig(),
|
||||
getWarnings: () => configService.getWarnings(),
|
||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||
onHotReloadApplied: applyConfigHotReloadDiff,
|
||||
defaultAnkiConnectUrl: DEFAULT_CONFIG.ankiConnect.url,
|
||||
createAnkiClient: (url) => new AnkiConnectClient(url),
|
||||
getYomitanAnkiDeckName: deps.getYomitanAnkiDeckName,
|
||||
getSettingsWindow: () => appState.configSettingsWindow,
|
||||
setSettingsWindow: (window) => {
|
||||
appState.configSettingsWindow = window as BrowserWindow | null;
|
||||
},
|
||||
createSettingsWindow: createCreateConfigSettingsWindowHandler({
|
||||
createBrowserWindow: (options) => deps.createBrowserWindow(options),
|
||||
preloadPath: path.join(deps.mainDirname, 'preload-settings.js'),
|
||||
}),
|
||||
settingsHtmlPath: path.join(deps.mainDirname, 'settings', 'index.html'),
|
||||
promoteSettingsWindowAboveOverlay: (window) =>
|
||||
promoteSettingsWindowAboveOverlay(window as BrowserWindow),
|
||||
openPath: (targetPath) => deps.openPath(targetPath),
|
||||
ipcMain: deps.ipcMain,
|
||||
ipcChannels: IPC_CHANNELS.request,
|
||||
log: (message) => logger.error(message),
|
||||
});
|
||||
|
||||
return {
|
||||
hotReload: configHotReloadRuntime,
|
||||
window: configSettingsRuntime,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user