mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract main runtime dependency builders
This commit is contained in:
611
src/main.ts
611
src/main.ts
@@ -148,6 +148,14 @@ import { createRunJellyfinCommandHandler } from './main/runtime/jellyfin-command
|
||||
import { createHandleJellyfinListCommands } from './main/runtime/jellyfin-cli-list';
|
||||
import { createHandleJellyfinPlayCommand } from './main/runtime/jellyfin-cli-play';
|
||||
import { createHandleJellyfinRemoteAnnounceCommand } from './main/runtime/jellyfin-cli-remote-announce';
|
||||
import {
|
||||
createGetJellyfinClientInfoHandler,
|
||||
createGetResolvedJellyfinConfigHandler,
|
||||
} from './main/runtime/jellyfin-client-info';
|
||||
import {
|
||||
createApplyJellyfinMpvDefaultsHandler,
|
||||
createGetDefaultSocketPathHandler,
|
||||
} from './main/runtime/mpv-jellyfin-defaults';
|
||||
import { createPlayJellyfinItemInMpvHandler } from './main/runtime/jellyfin-playback-launch';
|
||||
import { createPreloadJellyfinExternalSubtitlesHandler } from './main/runtime/jellyfin-subtitle-preload';
|
||||
import {
|
||||
@@ -157,12 +165,22 @@ import {
|
||||
import { createHandleInitialArgsHandler } from './main/runtime/initial-args-handler';
|
||||
import { createBuildHandleInitialArgsMainDepsHandler } from './main/runtime/initial-args-main-deps';
|
||||
import { createHandleTexthookerOnlyModeTransitionHandler } from './main/runtime/cli-command-prechecks';
|
||||
import { createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler } from './main/runtime/cli-command-prechecks-main-deps';
|
||||
import {
|
||||
createGetFieldGroupingResolverHandler,
|
||||
createSetFieldGroupingResolverHandler,
|
||||
} from './main/runtime/field-grouping-resolver';
|
||||
import { createCliCommandContext } from './main/runtime/cli-command-context';
|
||||
import { createBindMpvMainEventHandlersHandler } from './main/runtime/mpv-main-event-bindings';
|
||||
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './main/runtime/mpv-main-event-main-deps';
|
||||
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from './main/runtime/mpv-client-runtime-service-main-deps';
|
||||
import { createMpvClientRuntimeServiceFactory } from './main/runtime/mpv-client-runtime-service';
|
||||
import { createUpdateMpvSubtitleRenderMetricsHandler } from './main/runtime/mpv-subtitle-render-metrics';
|
||||
import {
|
||||
createBuildTokenizerDepsMainHandler,
|
||||
createCreateMecabTokenizerAndCheckMainHandler,
|
||||
createPrewarmSubtitleDictionariesMainHandler,
|
||||
} from './main/runtime/subtitle-tokenization-main-deps';
|
||||
import {
|
||||
createLaunchBackgroundWarmupTaskHandler,
|
||||
createStartBackgroundWarmupsHandler,
|
||||
@@ -192,6 +210,7 @@ import {
|
||||
createBuildAppendToMpvLogMainDepsHandler,
|
||||
createBuildShowMpvOsdMainDepsHandler,
|
||||
} from './main/runtime/mpv-osd-log-main-deps';
|
||||
import { createBuildCycleSecondarySubModeMainDepsHandler } from './main/runtime/secondary-sub-mode-main-deps';
|
||||
import {
|
||||
createCancelNumericShortcutSessionHandler,
|
||||
createStartNumericShortcutSessionHandler,
|
||||
@@ -226,6 +245,14 @@ import {
|
||||
createSetOverlayVisibleHandler,
|
||||
createToggleOverlayHandler,
|
||||
} from './main/runtime/overlay-main-actions';
|
||||
import {
|
||||
createBroadcastRuntimeOptionsChangedHandler,
|
||||
createGetRuntimeOptionsStateHandler,
|
||||
createOpenRuntimeOptionsPaletteHandler,
|
||||
createRestorePreviousSecondarySubVisibilityHandler,
|
||||
createSendToActiveOverlayWindowHandler,
|
||||
createSetOverlayDebugVisualizationEnabledHandler,
|
||||
} from './main/runtime/overlay-runtime-main-actions';
|
||||
import {
|
||||
createHandleMpvCommandFromIpcHandler,
|
||||
createRunSubsyncManualFromIpcHandler,
|
||||
@@ -279,6 +306,13 @@ import {
|
||||
createConfigHotReloadMessageHandler,
|
||||
resolveSubtitleStyleForRenderer,
|
||||
} from './main/runtime/config-hot-reload-handlers';
|
||||
import {
|
||||
createBuildCriticalConfigErrorMainDepsHandler,
|
||||
createBuildReloadConfigMainDepsHandler,
|
||||
} from './main/runtime/startup-config-main-deps';
|
||||
import { createBuildAppReadyRuntimeMainDepsHandler } from './main/runtime/app-ready-main-deps';
|
||||
import { createBuildAppLifecycleRuntimeRunnerMainDepsHandler } from './main/runtime/startup-lifecycle-main-deps';
|
||||
import { createBuildStartupBootstrapMainDepsHandler } from './main/runtime/startup-bootstrap-main-deps';
|
||||
import {
|
||||
enforceUnsupportedWaylandMode,
|
||||
forceX11Backend,
|
||||
@@ -429,14 +463,13 @@ let jellyfinMpvAutoLaunchInFlight: Promise<boolean> | null = null;
|
||||
let backgroundWarmupsStarted = false;
|
||||
let yomitanLoadInFlight: Promise<Extension | null> | null = null;
|
||||
|
||||
const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler({
|
||||
sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client as never, command),
|
||||
jellyfinLangPref: JELLYFIN_LANG_PREF,
|
||||
});
|
||||
|
||||
function applyJellyfinMpvDefaults(client: MpvIpcClient): void {
|
||||
sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']);
|
||||
sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']);
|
||||
sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']);
|
||||
sendMpvCommandRuntime(client, ['set_property', 'secondary-sid', 'auto']);
|
||||
sendMpvCommandRuntime(client, ['set_property', 'secondary-sub-visibility', 'no']);
|
||||
sendMpvCommandRuntime(client, ['set_property', 'alang', JELLYFIN_LANG_PREF]);
|
||||
sendMpvCommandRuntime(client, ['set_property', 'slang', JELLYFIN_LANG_PREF]);
|
||||
applyJellyfinMpvDefaultsHandler(client);
|
||||
}
|
||||
|
||||
const CONFIG_DIR = resolveConfigDir({
|
||||
@@ -493,11 +526,12 @@ const appLogger = {
|
||||
},
|
||||
};
|
||||
|
||||
const getDefaultSocketPathHandler = createGetDefaultSocketPathHandler({
|
||||
platform: process.platform,
|
||||
});
|
||||
|
||||
function getDefaultSocketPath(): string {
|
||||
if (process.platform === 'win32') {
|
||||
return '\\\\.\\pipe\\subminer-socket';
|
||||
}
|
||||
return '/tmp/subminer-socket';
|
||||
return getDefaultSocketPathHandler();
|
||||
}
|
||||
|
||||
if (!fs.existsSync(USER_DATA_PATH)) {
|
||||
@@ -795,23 +829,29 @@ const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService({
|
||||
},
|
||||
});
|
||||
|
||||
const getFieldGroupingResolverHandler = createGetFieldGroupingResolverHandler({
|
||||
getResolver: () => appState.fieldGroupingResolver,
|
||||
});
|
||||
|
||||
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
||||
return appState.fieldGroupingResolver;
|
||||
return getFieldGroupingResolverHandler();
|
||||
}
|
||||
|
||||
const setFieldGroupingResolverHandler = createSetFieldGroupingResolverHandler({
|
||||
setResolver: (resolver) => {
|
||||
appState.fieldGroupingResolver = resolver;
|
||||
},
|
||||
nextSequence: () => {
|
||||
appState.fieldGroupingResolverSequence += 1;
|
||||
return appState.fieldGroupingResolverSequence;
|
||||
},
|
||||
getSequence: () => appState.fieldGroupingResolverSequence,
|
||||
});
|
||||
|
||||
function setFieldGroupingResolver(
|
||||
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
|
||||
): void {
|
||||
if (!resolver) {
|
||||
appState.fieldGroupingResolver = null;
|
||||
return;
|
||||
}
|
||||
const sequence = ++appState.fieldGroupingResolverSequence;
|
||||
const wrappedResolver = (choice: KikuFieldGroupingChoice): void => {
|
||||
if (sequence !== appState.fieldGroupingResolverSequence) return;
|
||||
resolver(choice);
|
||||
};
|
||||
appState.fieldGroupingResolver = wrappedResolver;
|
||||
setFieldGroupingResolverHandler(resolver);
|
||||
}
|
||||
|
||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>({
|
||||
@@ -880,71 +920,97 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService({
|
||||
},
|
||||
});
|
||||
|
||||
const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler({
|
||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||
});
|
||||
|
||||
function getRuntimeOptionsState(): RuntimeOptionState[] {
|
||||
if (!appState.runtimeOptionsManager) return [];
|
||||
return appState.runtimeOptionsManager.listOptions();
|
||||
return getRuntimeOptionsStateHandler();
|
||||
}
|
||||
|
||||
function getOverlayWindows(): BrowserWindow[] {
|
||||
return overlayManager.getOverlayWindows();
|
||||
}
|
||||
|
||||
const restorePreviousSecondarySubVisibilityHandler = createRestorePreviousSecondarySubVisibilityHandler(
|
||||
{
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
},
|
||||
);
|
||||
|
||||
function restorePreviousSecondarySubVisibility(): void {
|
||||
if (!appState.mpvClient || !appState.mpvClient.connected) return;
|
||||
appState.mpvClient.restorePreviousSecondarySubVisibility();
|
||||
restorePreviousSecondarySubVisibilityHandler();
|
||||
}
|
||||
|
||||
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
||||
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
||||
}
|
||||
|
||||
const broadcastRuntimeOptionsChangedHandler = createBroadcastRuntimeOptionsChangedHandler({
|
||||
broadcastRuntimeOptionsChangedRuntime,
|
||||
getRuntimeOptionsState: () => getRuntimeOptionsState(),
|
||||
broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||
});
|
||||
|
||||
function broadcastRuntimeOptionsChanged(): void {
|
||||
broadcastRuntimeOptionsChangedRuntime(
|
||||
() => getRuntimeOptionsState(),
|
||||
(channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||
);
|
||||
broadcastRuntimeOptionsChangedHandler();
|
||||
}
|
||||
|
||||
const sendToActiveOverlayWindowHandler = createSendToActiveOverlayWindowHandler({
|
||||
sendToActiveOverlayWindowRuntime: (channel, payload, runtimeOptions) =>
|
||||
overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||
});
|
||||
|
||||
function sendToActiveOverlayWindow(
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||
): boolean {
|
||||
return overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions);
|
||||
return sendToActiveOverlayWindowHandler(channel, payload, runtimeOptions);
|
||||
}
|
||||
|
||||
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
||||
setOverlayDebugVisualizationEnabledRuntime(
|
||||
appState.overlayDebugVisualizationEnabled,
|
||||
enabled,
|
||||
(next) => {
|
||||
const setOverlayDebugVisualizationEnabledHandler = createSetOverlayDebugVisualizationEnabledHandler(
|
||||
{
|
||||
setOverlayDebugVisualizationEnabledRuntime,
|
||||
getCurrentEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||
setCurrentEnabled: (next) => {
|
||||
appState.overlayDebugVisualizationEnabled = next;
|
||||
},
|
||||
(channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||
);
|
||||
broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||
},
|
||||
);
|
||||
|
||||
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
||||
setOverlayDebugVisualizationEnabledHandler(enabled);
|
||||
}
|
||||
|
||||
const openRuntimeOptionsPaletteHandler = createOpenRuntimeOptionsPaletteHandler({
|
||||
openRuntimeOptionsPaletteRuntime: () => overlayModalRuntime.openRuntimeOptionsPalette(),
|
||||
});
|
||||
|
||||
function openRuntimeOptionsPalette(): void {
|
||||
overlayModalRuntime.openRuntimeOptionsPalette();
|
||||
openRuntimeOptionsPaletteHandler();
|
||||
}
|
||||
|
||||
function getResolvedConfig() {
|
||||
return configService.getConfig();
|
||||
}
|
||||
|
||||
const getResolvedJellyfinConfigHandler = createGetResolvedJellyfinConfigHandler({
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
});
|
||||
|
||||
function getResolvedJellyfinConfig() {
|
||||
return getResolvedConfig().jellyfin;
|
||||
return getResolvedJellyfinConfigHandler();
|
||||
}
|
||||
|
||||
const getJellyfinClientInfoHandler = createGetJellyfinClientInfoHandler({
|
||||
getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
getDefaultJellyfinConfig: () => DEFAULT_CONFIG.jellyfin,
|
||||
});
|
||||
|
||||
function getJellyfinClientInfo(config = getResolvedJellyfinConfig()) {
|
||||
const clientName = config.clientName || DEFAULT_CONFIG.jellyfin.clientName;
|
||||
const clientVersion = config.clientVersion || DEFAULT_CONFIG.jellyfin.clientVersion;
|
||||
const deviceId = config.deviceId || DEFAULT_CONFIG.jellyfin.deviceId;
|
||||
return {
|
||||
clientName,
|
||||
clientVersion,
|
||||
deviceId,
|
||||
};
|
||||
return getJellyfinClientInfoHandler(config);
|
||||
}
|
||||
|
||||
const waitForMpvConnected = createWaitForMpvConnectedHandler({
|
||||
@@ -1553,162 +1619,176 @@ const restoreWindowsOnActivateHandler = createRestoreWindowsOnActivateHandler(
|
||||
})(),
|
||||
);
|
||||
|
||||
const buildStartupBootstrapRuntimeFactoryDepsHandler =
|
||||
createBuildStartupBootstrapRuntimeFactoryDepsHandler({
|
||||
argv: process.argv,
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||
setLogLevel(level, source);
|
||||
const reloadConfigHandler = createReloadConfigHandler(
|
||||
createBuildReloadConfigMainDepsHandler({
|
||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||
logInfo: (message) => appLogger.logInfo(message),
|
||||
logWarning: (message) => appLogger.logWarning(message),
|
||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||
startConfigHotReload: () => configHotReloadRuntime.start(),
|
||||
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
|
||||
failHandlers: {
|
||||
logError: (details) => logger.error(details),
|
||||
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
|
||||
quit: () => app.quit(),
|
||||
},
|
||||
forceX11Backend: (args: CliArgs) => {
|
||||
forceX11Backend(args);
|
||||
})(),
|
||||
);
|
||||
|
||||
const criticalConfigErrorHandler = createCriticalConfigErrorHandler(
|
||||
createBuildCriticalConfigErrorMainDepsHandler({
|
||||
getConfigPath: () => configService.getConfigPath(),
|
||||
failHandlers: {
|
||||
logError: (message) => logger.error(message),
|
||||
showErrorBox: (title, message) => dialog.showErrorBox(title, message),
|
||||
quit: () => app.quit(),
|
||||
},
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
||||
enforceUnsupportedWaylandMode(args);
|
||||
})(),
|
||||
);
|
||||
|
||||
const appReadyRuntimeRunner = createAppReadyRuntimeRunner(
|
||||
createBuildAppReadyRuntimeMainDepsHandler({
|
||||
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||
resolveKeybindings: () => {
|
||||
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||
},
|
||||
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
configDir: CONFIG_DIR,
|
||||
defaultConfig: DEFAULT_CONFIG,
|
||||
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
||||
generateDefaultConfigFile: (
|
||||
args: CliArgs,
|
||||
options: {
|
||||
configDir: string;
|
||||
defaultConfig: unknown;
|
||||
generateTemplate: (config: unknown) => string;
|
||||
createMpvClient: () => {
|
||||
appState.mpvClient = createMpvClientRuntimeService();
|
||||
},
|
||||
reloadConfig: reloadConfigHandler,
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => setLogLevel(level, source),
|
||||
initRuntimeOptionsManager: () => {
|
||||
appState.runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
{
|
||||
applyAnkiPatch: (patch) => {
|
||||
if (appState.ankiIntegration) {
|
||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
onOptionsChanged: () => {
|
||||
broadcastRuntimeOptionsChanged();
|
||||
refreshOverlayShortcuts();
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
appState.secondarySubMode = mode;
|
||||
},
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
|
||||
startSubtitleWebsocket: (port: number) => {
|
||||
subtitleWsService.start(port, () => appState.currentSubText);
|
||||
},
|
||||
log: (message) => appLogger.logInfo(message),
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
await createMecabTokenizerAndCheck();
|
||||
},
|
||||
createSubtitleTimingTracker: () => {
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
appState.subtitleTimingTracker = tracker;
|
||||
},
|
||||
createImmersionTracker: createImmersionTrackerStartupHandler({
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
||||
createTrackerService: (params) => new ImmersionTrackerService(params),
|
||||
setTracker: (tracker) => {
|
||||
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
||||
},
|
||||
) => generateDefaultConfigFile(args, options),
|
||||
onConfigGenerated: (exitCode: number) => {
|
||||
process.exitCode = exitCode;
|
||||
app.quit();
|
||||
},
|
||||
onGenerateConfigError: (error: Error) => {
|
||||
logger.error(`Failed to generate config: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
app.quit();
|
||||
},
|
||||
startAppLifecycle: createAppLifecycleRuntimeRunner({
|
||||
app,
|
||||
platform: process.platform,
|
||||
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
||||
handleCliCommand(nextArgs, source),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||
onReady: createAppReadyRuntimeRunner({
|
||||
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||
resolveKeybindings: () => {
|
||||
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||
},
|
||||
createMpvClient: () => {
|
||||
appState.mpvClient = createMpvClientRuntimeService();
|
||||
},
|
||||
reloadConfig: createReloadConfigHandler({
|
||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||
logInfo: (message) => appLogger.logInfo(message),
|
||||
logWarning: (message) => appLogger.logWarning(message),
|
||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||
startConfigHotReload: () => configHotReloadRuntime.start(),
|
||||
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
|
||||
failHandlers: {
|
||||
logError: (details) => logger.error(details),
|
||||
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
|
||||
quit: () => app.quit(),
|
||||
},
|
||||
}),
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => setLogLevel(level, source),
|
||||
initRuntimeOptionsManager: () => {
|
||||
appState.runtimeOptionsManager = new RuntimeOptionsManager(
|
||||
() => configService.getConfig().ankiConnect,
|
||||
{
|
||||
applyAnkiPatch: (patch) => {
|
||||
if (appState.ankiIntegration) {
|
||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
onOptionsChanged: () => {
|
||||
broadcastRuntimeOptionsChanged();
|
||||
refreshOverlayShortcuts();
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
appState.secondarySubMode = mode;
|
||||
},
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
|
||||
startSubtitleWebsocket: (port: number) => {
|
||||
subtitleWsService.start(port, () => appState.currentSubText);
|
||||
},
|
||||
log: (message) => appLogger.logInfo(message),
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
await createMecabTokenizerAndCheck();
|
||||
},
|
||||
createSubtitleTimingTracker: () => {
|
||||
const tracker = new SubtitleTimingTracker();
|
||||
appState.subtitleTimingTracker = tracker;
|
||||
},
|
||||
createImmersionTracker: createImmersionTrackerStartupHandler({
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
||||
createTrackerService: (params) => new ImmersionTrackerService(params),
|
||||
setTracker: (tracker) => {
|
||||
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
||||
},
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
seedTrackerFromCurrentMedia: () => {
|
||||
void immersionMediaRuntime.seedFromCurrentMedia();
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
logWarn: (message, details) => logger.warn(message, details),
|
||||
}),
|
||||
loadYomitanExtension: async () => {
|
||||
await loadYomitanExtension();
|
||||
},
|
||||
startJellyfinRemoteSession: async () => {
|
||||
await startJellyfinRemoteSession();
|
||||
},
|
||||
prewarmSubtitleDictionaries: async () => {
|
||||
await prewarmSubtitleDictionaries();
|
||||
},
|
||||
startBackgroundWarmups: () => {
|
||||
startBackgroundWarmups();
|
||||
},
|
||||
texthookerOnlyMode: appState.texthookerOnlyMode,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
|
||||
appState.backgroundMode
|
||||
? false
|
||||
: configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
handleInitialArgs: () => handleInitialArgs(),
|
||||
onCriticalConfigErrors: createCriticalConfigErrorHandler({
|
||||
getConfigPath: () => configService.getConfigPath(),
|
||||
failHandlers: {
|
||||
logError: (message) => logger.error(message),
|
||||
showErrorBox: (title, message) => dialog.showErrorBox(title, message),
|
||||
quit: () => app.quit(),
|
||||
},
|
||||
}),
|
||||
logDebug: (message: string) => {
|
||||
logger.debug(message);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
}),
|
||||
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
||||
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
||||
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
||||
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
seedTrackerFromCurrentMedia: () => {
|
||||
void immersionMediaRuntime.seedFromCurrentMedia();
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
logDebug: (message) => logger.debug(message),
|
||||
logWarn: (message, details) => logger.warn(message, details),
|
||||
}),
|
||||
});
|
||||
loadYomitanExtension: async () => {
|
||||
await loadYomitanExtension();
|
||||
},
|
||||
startJellyfinRemoteSession: async () => {
|
||||
await startJellyfinRemoteSession();
|
||||
},
|
||||
prewarmSubtitleDictionaries: async () => {
|
||||
await prewarmSubtitleDictionaries();
|
||||
},
|
||||
startBackgroundWarmups: () => {
|
||||
startBackgroundWarmups();
|
||||
},
|
||||
texthookerOnlyMode: appState.texthookerOnlyMode,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
|
||||
appState.backgroundMode
|
||||
? false
|
||||
: configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
handleInitialArgs: () => handleInitialArgs(),
|
||||
onCriticalConfigErrors: criticalConfigErrorHandler,
|
||||
logDebug: (message: string) => {
|
||||
logger.debug(message);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
})(),
|
||||
);
|
||||
|
||||
const appLifecycleRuntimeRunner = createAppLifecycleRuntimeRunner(
|
||||
createBuildAppLifecycleRuntimeRunnerMainDepsHandler({
|
||||
app,
|
||||
platform: process.platform,
|
||||
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
||||
handleCliCommand(nextArgs, source),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||
onReady: appReadyRuntimeRunner,
|
||||
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
||||
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
||||
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
||||
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
||||
})(),
|
||||
);
|
||||
|
||||
const buildStartupBootstrapRuntimeFactoryDepsHandler =
|
||||
createBuildStartupBootstrapRuntimeFactoryDepsHandler(
|
||||
createBuildStartupBootstrapMainDepsHandler({
|
||||
argv: process.argv,
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||
setLogLevel(level, source);
|
||||
},
|
||||
forceX11Backend: (args: CliArgs) => {
|
||||
forceX11Backend(args);
|
||||
},
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
||||
enforceUnsupportedWaylandMode(args);
|
||||
},
|
||||
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
configDir: CONFIG_DIR,
|
||||
defaultConfig: DEFAULT_CONFIG,
|
||||
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
||||
generateDefaultConfigFile: (
|
||||
args: CliArgs,
|
||||
options: {
|
||||
configDir: string;
|
||||
defaultConfig: unknown;
|
||||
generateTemplate: (config: unknown) => string;
|
||||
},
|
||||
) => generateDefaultConfigFile(args, options),
|
||||
setExitCode: (code) => {
|
||||
process.exitCode = code;
|
||||
},
|
||||
quitApp: () => app.quit(),
|
||||
logGenerateConfigError: (message) => logger.error(message),
|
||||
startAppLifecycle: appLifecycleRuntimeRunner,
|
||||
})(),
|
||||
);
|
||||
|
||||
const startupState = runStartupBootstrapRuntime(
|
||||
createStartupBootstrapRuntimeDeps(buildStartupBootstrapRuntimeFactoryDepsHandler()),
|
||||
@@ -1718,16 +1798,20 @@ applyStartupState(appState, startupState);
|
||||
void refreshAnilistClientSecretState({ force: true });
|
||||
anilistStateRuntime.refreshRetryQueueState();
|
||||
|
||||
function handleCliCommand(args: CliArgs, source: CliCommandSource = 'initial'): void {
|
||||
createHandleTexthookerOnlyModeTransitionHandler({
|
||||
const handleTexthookerOnlyModeTransitionHandler = createHandleTexthookerOnlyModeTransitionHandler(
|
||||
createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler({
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
setTexthookerOnlyMode: (enabled) => {
|
||||
appState.texthookerOnlyMode = enabled;
|
||||
},
|
||||
commandNeedsOverlayRuntime: (inputArgs) => commandNeedsOverlayRuntime(inputArgs),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
logInfo: (message) => logger.info(message),
|
||||
})(args);
|
||||
logInfo: (message: string) => logger.info(message),
|
||||
})(),
|
||||
);
|
||||
|
||||
function handleCliCommand(args: CliArgs, source: CliCommandSource = 'initial'): void {
|
||||
handleTexthookerOnlyModeTransitionHandler(args);
|
||||
|
||||
const cliContext = createCliCommandContext(buildCliCommandContextDepsHandler());
|
||||
handleCliCommandRuntimeServiceWithContext(args, source, cliContext);
|
||||
@@ -1833,59 +1917,62 @@ function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>
|
||||
updateMpvSubtitleRenderMetricsRuntime(patch);
|
||||
}
|
||||
|
||||
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler({
|
||||
getYomitanExt: () => appState.yomitanExt,
|
||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
appState.yomitanParserWindow = window as BrowserWindow | null;
|
||||
},
|
||||
getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
appState.yomitanParserReadyPromise = promise;
|
||||
},
|
||||
getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
appState.yomitanParserInitPromise = promise;
|
||||
},
|
||||
isKnownWord: (text) => Boolean(appState.ankiIntegration?.isKnownWord(text)),
|
||||
recordLookup: (hit) => {
|
||||
appState.immersionTracker?.recordLookup(hit);
|
||||
},
|
||||
getKnownWordMatchMode: () =>
|
||||
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
||||
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
||||
getMinSentenceWordsForNPlusOne: () => getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
||||
getFrequencyDictionaryEnabled: () => getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
||||
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||
});
|
||||
|
||||
const createMecabTokenizerAndCheckHandler = createCreateMecabTokenizerAndCheckMainHandler({
|
||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||
setMecabTokenizer: (tokenizer) => {
|
||||
appState.mecabTokenizer = tokenizer;
|
||||
},
|
||||
createMecabTokenizer: () => new MecabTokenizer(),
|
||||
checkAvailability: async (tokenizer) => tokenizer.checkAvailability(),
|
||||
});
|
||||
|
||||
const prewarmSubtitleDictionariesHandler = createPrewarmSubtitleDictionariesMainHandler({
|
||||
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
||||
ensureFrequencyDictionaryLookup: () => frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
||||
});
|
||||
|
||||
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
||||
await jlptDictionaryRuntime.ensureJlptDictionaryLookup();
|
||||
await frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup();
|
||||
return tokenizeSubtitleCore(
|
||||
text,
|
||||
createTokenizerDepsRuntime({
|
||||
getYomitanExt: () => appState.yomitanExt,
|
||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
appState.yomitanParserWindow = window;
|
||||
},
|
||||
getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
appState.yomitanParserReadyPromise = promise;
|
||||
},
|
||||
getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
appState.yomitanParserInitPromise = promise;
|
||||
},
|
||||
isKnownWord: (text) =>
|
||||
(() => {
|
||||
const hit = Boolean(appState.ankiIntegration?.isKnownWord(text));
|
||||
appState.immersionTracker?.recordLookup(hit);
|
||||
return hit;
|
||||
})(),
|
||||
getKnownWordMatchMode: () =>
|
||||
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
||||
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
||||
getMinSentenceWordsForNPlusOne: () =>
|
||||
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
||||
getFrequencyDictionaryEnabled: () =>
|
||||
getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
||||
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||
}),
|
||||
);
|
||||
return tokenizeSubtitleCore(text, createTokenizerDepsRuntime(buildTokenizerDepsHandler()));
|
||||
}
|
||||
|
||||
async function createMecabTokenizerAndCheck(): Promise<void> {
|
||||
if (!appState.mecabTokenizer) {
|
||||
appState.mecabTokenizer = new MecabTokenizer();
|
||||
}
|
||||
await appState.mecabTokenizer.checkAvailability();
|
||||
await createMecabTokenizerAndCheckHandler();
|
||||
}
|
||||
|
||||
async function prewarmSubtitleDictionaries(): Promise<void> {
|
||||
await Promise.all([
|
||||
jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
||||
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
||||
]);
|
||||
await prewarmSubtitleDictionariesHandler();
|
||||
}
|
||||
|
||||
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskHandler({
|
||||
@@ -2020,20 +2107,22 @@ function getConfiguredShortcuts() {
|
||||
}
|
||||
|
||||
function cycleSecondarySubMode(): void {
|
||||
cycleSecondarySubModeCore({
|
||||
getSecondarySubMode: () => appState.secondarySubMode,
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
appState.secondarySubMode = mode;
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||
appState.lastSecondarySubToggleAtMs = timestampMs;
|
||||
},
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
broadcastToOverlayWindows('secondary-subtitle:mode', mode);
|
||||
},
|
||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||
});
|
||||
cycleSecondarySubModeCore(
|
||||
createBuildCycleSecondarySubModeMainDepsHandler({
|
||||
getSecondarySubMode: () => appState.secondarySubMode,
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||
appState.secondarySubMode = mode;
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||
appState.lastSecondarySubToggleAtMs = timestampMs;
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, mode) => {
|
||||
broadcastToOverlayWindows(channel, mode);
|
||||
},
|
||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||
})(),
|
||||
);
|
||||
}
|
||||
|
||||
const appendToMpvLogHandler = createAppendToMpvLogHandler({
|
||||
|
||||
71
src/main/runtime/app-ready-main-deps.test.ts
Normal file
71
src/main/runtime/app-ready-main-deps.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildAppReadyRuntimeMainDepsHandler } from './app-ready-main-deps';
|
||||
|
||||
test('app-ready main deps builder returns mapped app-ready runtime deps', async () => {
|
||||
const calls: string[] = [];
|
||||
const onReady = createBuildAppReadyRuntimeMainDepsHandler({
|
||||
loadSubtitlePosition: () => calls.push('load-subtitle-position'),
|
||||
resolveKeybindings: () => calls.push('resolve-keybindings'),
|
||||
createMpvClient: () => calls.push('create-mpv-client'),
|
||||
reloadConfig: () => calls.push('reload-config'),
|
||||
getResolvedConfig: () => ({ websocket: {} }),
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => calls.push('log-config-warning'),
|
||||
initRuntimeOptionsManager: () => calls.push('init-runtime-options'),
|
||||
setSecondarySubMode: () => calls.push('set-secondary-sub-mode'),
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: 5174,
|
||||
hasMpvWebsocketPlugin: () => false,
|
||||
startSubtitleWebsocket: () => calls.push('start-ws'),
|
||||
log: () => calls.push('log'),
|
||||
setLogLevel: () => calls.push('set-log-level'),
|
||||
createMecabTokenizerAndCheck: async () => {
|
||||
calls.push('create-mecab');
|
||||
},
|
||||
createSubtitleTimingTracker: () => calls.push('create-subtitle-tracker'),
|
||||
createImmersionTracker: () => calls.push('create-immersion'),
|
||||
startJellyfinRemoteSession: async () => {
|
||||
calls.push('start-jellyfin');
|
||||
},
|
||||
loadYomitanExtension: async () => {
|
||||
calls.push('load-yomitan');
|
||||
},
|
||||
prewarmSubtitleDictionaries: async () => {
|
||||
calls.push('prewarm-dicts');
|
||||
},
|
||||
startBackgroundWarmups: () => calls.push('start-warmups'),
|
||||
texthookerOnlyMode: false,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => true,
|
||||
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
||||
handleInitialArgs: () => calls.push('handle-initial-args'),
|
||||
onCriticalConfigErrors: () => {
|
||||
throw new Error('should not call');
|
||||
},
|
||||
logDebug: () => calls.push('debug'),
|
||||
now: () => 123,
|
||||
})();
|
||||
|
||||
assert.equal(onReady.defaultSecondarySubMode, 'hover');
|
||||
assert.equal(onReady.defaultWebsocketPort, 5174);
|
||||
assert.equal(onReady.texthookerOnlyMode, false);
|
||||
assert.equal(onReady.shouldAutoInitializeOverlayRuntimeFromConfig(), true);
|
||||
assert.equal(onReady.now?.(), 123);
|
||||
onReady.loadSubtitlePosition();
|
||||
onReady.resolveKeybindings();
|
||||
onReady.createMpvClient();
|
||||
await onReady.createMecabTokenizerAndCheck();
|
||||
await onReady.loadYomitanExtension();
|
||||
await onReady.prewarmSubtitleDictionaries?.();
|
||||
onReady.startBackgroundWarmups();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'load-subtitle-position',
|
||||
'resolve-keybindings',
|
||||
'create-mpv-client',
|
||||
'create-mecab',
|
||||
'load-yomitan',
|
||||
'prewarm-dicts',
|
||||
'start-warmups',
|
||||
]);
|
||||
});
|
||||
38
src/main/runtime/app-ready-main-deps.ts
Normal file
38
src/main/runtime/app-ready-main-deps.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle';
|
||||
|
||||
export function createBuildAppReadyRuntimeMainDepsHandler(
|
||||
deps: AppReadyRuntimeDepsFactoryInput,
|
||||
) {
|
||||
return (): AppReadyRuntimeDepsFactoryInput => ({
|
||||
loadSubtitlePosition: deps.loadSubtitlePosition,
|
||||
resolveKeybindings: deps.resolveKeybindings,
|
||||
createMpvClient: deps.createMpvClient,
|
||||
reloadConfig: deps.reloadConfig,
|
||||
getResolvedConfig: deps.getResolvedConfig,
|
||||
getConfigWarnings: deps.getConfigWarnings,
|
||||
logConfigWarning: deps.logConfigWarning,
|
||||
initRuntimeOptionsManager: deps.initRuntimeOptionsManager,
|
||||
setSecondarySubMode: deps.setSecondarySubMode,
|
||||
defaultSecondarySubMode: deps.defaultSecondarySubMode,
|
||||
defaultWebsocketPort: deps.defaultWebsocketPort,
|
||||
hasMpvWebsocketPlugin: deps.hasMpvWebsocketPlugin,
|
||||
startSubtitleWebsocket: deps.startSubtitleWebsocket,
|
||||
log: deps.log,
|
||||
setLogLevel: deps.setLogLevel,
|
||||
createMecabTokenizerAndCheck: deps.createMecabTokenizerAndCheck,
|
||||
createSubtitleTimingTracker: deps.createSubtitleTimingTracker,
|
||||
createImmersionTracker: deps.createImmersionTracker,
|
||||
startJellyfinRemoteSession: deps.startJellyfinRemoteSession,
|
||||
loadYomitanExtension: deps.loadYomitanExtension,
|
||||
prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries,
|
||||
startBackgroundWarmups: deps.startBackgroundWarmups,
|
||||
texthookerOnlyMode: deps.texthookerOnlyMode,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig:
|
||||
deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
||||
handleInitialArgs: deps.handleInitialArgs,
|
||||
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
||||
logDebug: deps.logDebug,
|
||||
now: deps.now,
|
||||
});
|
||||
}
|
||||
21
src/main/runtime/cli-command-prechecks-main-deps.test.ts
Normal file
21
src/main/runtime/cli-command-prechecks-main-deps.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler } from './cli-command-prechecks-main-deps';
|
||||
|
||||
test('cli prechecks main deps builder maps transition handlers', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler({
|
||||
isTexthookerOnlyMode: () => true,
|
||||
setTexthookerOnlyMode: (enabled) => calls.push(`set:${enabled}`),
|
||||
commandNeedsOverlayRuntime: () => true,
|
||||
startBackgroundWarmups: () => calls.push('warmups'),
|
||||
logInfo: (message) => calls.push(`info:${message}`),
|
||||
})();
|
||||
|
||||
assert.equal(deps.isTexthookerOnlyMode(), true);
|
||||
assert.equal(deps.commandNeedsOverlayRuntime({} as never), true);
|
||||
deps.setTexthookerOnlyMode(false);
|
||||
deps.startBackgroundWarmups();
|
||||
deps.logInfo('x');
|
||||
assert.deepEqual(calls, ['set:false', 'warmups', 'info:x']);
|
||||
});
|
||||
17
src/main/runtime/cli-command-prechecks-main-deps.ts
Normal file
17
src/main/runtime/cli-command-prechecks-main-deps.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { CliArgs } from '../../cli/args';
|
||||
|
||||
export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(deps: {
|
||||
isTexthookerOnlyMode: () => boolean;
|
||||
setTexthookerOnlyMode: (enabled: boolean) => void;
|
||||
commandNeedsOverlayRuntime: (args: CliArgs) => boolean;
|
||||
startBackgroundWarmups: () => void;
|
||||
logInfo: (message: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(),
|
||||
setTexthookerOnlyMode: (enabled: boolean) => deps.setTexthookerOnlyMode(enabled),
|
||||
commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args),
|
||||
startBackgroundWarmups: () => deps.startBackgroundWarmups(),
|
||||
logInfo: (message: string) => deps.logInfo(message),
|
||||
});
|
||||
}
|
||||
56
src/main/runtime/field-grouping-resolver.test.ts
Normal file
56
src/main/runtime/field-grouping-resolver.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createGetFieldGroupingResolverHandler,
|
||||
createSetFieldGroupingResolverHandler,
|
||||
} from './field-grouping-resolver';
|
||||
|
||||
test('get field grouping resolver returns current resolver', () => {
|
||||
const resolver = () => undefined;
|
||||
const getResolver = createGetFieldGroupingResolverHandler({
|
||||
getResolver: () => resolver,
|
||||
});
|
||||
|
||||
assert.equal(getResolver(), resolver);
|
||||
});
|
||||
|
||||
test('set field grouping resolver clears resolver when null is provided', () => {
|
||||
let current: ((choice: unknown) => void) | null = () => undefined;
|
||||
const setResolver = createSetFieldGroupingResolverHandler({
|
||||
setResolver: (resolver) => {
|
||||
current = resolver as never;
|
||||
},
|
||||
nextSequence: () => 1,
|
||||
getSequence: () => 1,
|
||||
});
|
||||
|
||||
setResolver(null);
|
||||
assert.equal(current, null);
|
||||
});
|
||||
|
||||
test('set field grouping resolver wraps resolver and ignores stale sequence', () => {
|
||||
const calls: string[] = [];
|
||||
let current: ((choice: unknown) => void) | null = null;
|
||||
let sequence = 0;
|
||||
|
||||
const setResolver = createSetFieldGroupingResolverHandler({
|
||||
setResolver: (resolver) => {
|
||||
current = resolver as never;
|
||||
},
|
||||
nextSequence: () => {
|
||||
sequence += 1;
|
||||
return sequence;
|
||||
},
|
||||
getSequence: () => sequence,
|
||||
});
|
||||
|
||||
setResolver((choice) => calls.push(`new:${choice}`));
|
||||
const firstWrapped = current!;
|
||||
setResolver((choice) => calls.push(`latest:${choice}`));
|
||||
const latestWrapped = current!;
|
||||
|
||||
firstWrapped('A');
|
||||
latestWrapped('B');
|
||||
|
||||
assert.deepEqual(calls, ['latest:B']);
|
||||
});
|
||||
29
src/main/runtime/field-grouping-resolver.ts
Normal file
29
src/main/runtime/field-grouping-resolver.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { KikuFieldGroupingChoice } from '../../types';
|
||||
|
||||
type FieldGroupingResolver = ((choice: KikuFieldGroupingChoice) => void) | null;
|
||||
|
||||
export function createGetFieldGroupingResolverHandler(deps: {
|
||||
getResolver: () => FieldGroupingResolver;
|
||||
}) {
|
||||
return (): FieldGroupingResolver => deps.getResolver();
|
||||
}
|
||||
|
||||
export function createSetFieldGroupingResolverHandler(deps: {
|
||||
setResolver: (resolver: FieldGroupingResolver) => void;
|
||||
nextSequence: () => number;
|
||||
getSequence: () => number;
|
||||
}) {
|
||||
return (resolver: FieldGroupingResolver): void => {
|
||||
if (!resolver) {
|
||||
deps.setResolver(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const sequence = deps.nextSequence();
|
||||
const wrappedResolver = (choice: KikuFieldGroupingChoice): void => {
|
||||
if (sequence !== deps.getSequence()) return;
|
||||
resolver(choice);
|
||||
};
|
||||
deps.setResolver(wrappedResolver);
|
||||
};
|
||||
}
|
||||
56
src/main/runtime/jellyfin-client-info.test.ts
Normal file
56
src/main/runtime/jellyfin-client-info.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createGetJellyfinClientInfoHandler,
|
||||
createGetResolvedJellyfinConfigHandler,
|
||||
} from './jellyfin-client-info';
|
||||
|
||||
test('get resolved jellyfin config returns jellyfin section from resolved config', () => {
|
||||
const jellyfin = { url: 'https://jellyfin.local' } as never;
|
||||
const getConfig = createGetResolvedJellyfinConfigHandler({
|
||||
getResolvedConfig: () => ({ jellyfin } as never),
|
||||
});
|
||||
|
||||
assert.equal(getConfig(), jellyfin);
|
||||
});
|
||||
|
||||
test('jellyfin client info resolves defaults when fields are missing', () => {
|
||||
const getClientInfo = createGetJellyfinClientInfoHandler({
|
||||
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never),
|
||||
getDefaultJellyfinConfig: () =>
|
||||
({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0.0',
|
||||
deviceId: 'default-device',
|
||||
}) as never,
|
||||
});
|
||||
|
||||
assert.deepEqual(getClientInfo(), {
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0.0',
|
||||
deviceId: 'default-device',
|
||||
});
|
||||
});
|
||||
|
||||
test('jellyfin client info keeps explicit config values', () => {
|
||||
const getClientInfo = createGetJellyfinClientInfoHandler({
|
||||
getResolvedJellyfinConfig: () =>
|
||||
({
|
||||
clientName: 'Custom',
|
||||
clientVersion: '2.3.4',
|
||||
deviceId: 'custom-device',
|
||||
}) as never,
|
||||
getDefaultJellyfinConfig: () =>
|
||||
({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0.0',
|
||||
deviceId: 'default-device',
|
||||
}) as never,
|
||||
});
|
||||
|
||||
assert.deepEqual(getClientInfo(), {
|
||||
clientName: 'Custom',
|
||||
clientVersion: '2.3.4',
|
||||
deviceId: 'custom-device',
|
||||
});
|
||||
});
|
||||
33
src/main/runtime/jellyfin-client-info.ts
Normal file
33
src/main/runtime/jellyfin-client-info.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||
getResolvedConfig: () => { jellyfin: unknown };
|
||||
}) {
|
||||
return () => deps.getResolvedConfig().jellyfin as never;
|
||||
}
|
||||
|
||||
export function createGetJellyfinClientInfoHandler(deps: {
|
||||
getResolvedJellyfinConfig: () => {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
getDefaultJellyfinConfig: () => {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
config = deps.getResolvedJellyfinConfig(),
|
||||
): {
|
||||
clientName: string;
|
||||
clientVersion: string;
|
||||
deviceId: string;
|
||||
} => {
|
||||
const defaults = deps.getDefaultJellyfinConfig();
|
||||
return {
|
||||
clientName: config.clientName || defaults.clientName || '',
|
||||
clientVersion: config.clientVersion || defaults.clientVersion || '',
|
||||
deviceId: config.deviceId || defaults.deviceId || '',
|
||||
};
|
||||
};
|
||||
}
|
||||
32
src/main/runtime/mpv-jellyfin-defaults.test.ts
Normal file
32
src/main/runtime/mpv-jellyfin-defaults.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createApplyJellyfinMpvDefaultsHandler,
|
||||
createGetDefaultSocketPathHandler,
|
||||
} from './mpv-jellyfin-defaults';
|
||||
|
||||
test('apply jellyfin mpv defaults sends expected property commands', () => {
|
||||
const calls: string[] = [];
|
||||
const applyDefaults = createApplyJellyfinMpvDefaultsHandler({
|
||||
sendMpvCommandRuntime: (_client, command) => calls.push(command.join(':')),
|
||||
jellyfinLangPref: 'ja,jp',
|
||||
});
|
||||
|
||||
applyDefaults({});
|
||||
assert.deepEqual(calls, [
|
||||
'set_property:sub-auto:fuzzy',
|
||||
'set_property:aid:auto',
|
||||
'set_property:sid:auto',
|
||||
'set_property:secondary-sid:auto',
|
||||
'set_property:secondary-sub-visibility:no',
|
||||
'set_property:alang:ja,jp',
|
||||
'set_property:slang:ja,jp',
|
||||
]);
|
||||
});
|
||||
|
||||
test('get default socket path returns platform specific value', () => {
|
||||
const getWindowsPath = createGetDefaultSocketPathHandler({ platform: 'win32' });
|
||||
const getUnixPath = createGetDefaultSocketPathHandler({ platform: 'darwin' });
|
||||
assert.equal(getWindowsPath(), '\\\\.\\pipe\\subminer-socket');
|
||||
assert.equal(getUnixPath(), '/tmp/subminer-socket');
|
||||
});
|
||||
30
src/main/runtime/mpv-jellyfin-defaults.ts
Normal file
30
src/main/runtime/mpv-jellyfin-defaults.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
type MpvClientLike = unknown;
|
||||
|
||||
export function createApplyJellyfinMpvDefaultsHandler(deps: {
|
||||
sendMpvCommandRuntime: (
|
||||
client: MpvClientLike,
|
||||
command: [string, string, string],
|
||||
) => void;
|
||||
jellyfinLangPref: string;
|
||||
}) {
|
||||
return (client: MpvClientLike): void => {
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'secondary-sid', 'auto']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'secondary-sub-visibility', 'no']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'alang', deps.jellyfinLangPref]);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'slang', deps.jellyfinLangPref]);
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetDefaultSocketPathHandler(deps: {
|
||||
platform: string;
|
||||
}) {
|
||||
return (): string => {
|
||||
if (deps.platform === 'win32') {
|
||||
return '\\\\.\\pipe\\subminer-socket';
|
||||
}
|
||||
return '/tmp/subminer-socket';
|
||||
};
|
||||
}
|
||||
134
src/main/runtime/overlay-runtime-main-actions.test.ts
Normal file
134
src/main/runtime/overlay-runtime-main-actions.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createBroadcastRuntimeOptionsChangedHandler,
|
||||
createGetRuntimeOptionsStateHandler,
|
||||
createOpenRuntimeOptionsPaletteHandler,
|
||||
createRestorePreviousSecondarySubVisibilityHandler,
|
||||
createSendToActiveOverlayWindowHandler,
|
||||
createSetOverlayDebugVisualizationEnabledHandler,
|
||||
} from './overlay-runtime-main-actions';
|
||||
|
||||
test('runtime options state handler returns empty list without manager', () => {
|
||||
const getState = createGetRuntimeOptionsStateHandler({
|
||||
getRuntimeOptionsManager: () => null,
|
||||
});
|
||||
assert.deepEqual(getState(), []);
|
||||
});
|
||||
|
||||
test('runtime options state handler returns list from manager', () => {
|
||||
const getState = createGetRuntimeOptionsStateHandler({
|
||||
getRuntimeOptionsManager: () =>
|
||||
({
|
||||
listOptions: () => [
|
||||
{
|
||||
id: 'anki.autoUpdateNewCards',
|
||||
label: 'X',
|
||||
scope: 'ankiConnect',
|
||||
valueType: 'boolean',
|
||||
value: true,
|
||||
allowedValues: [true, false],
|
||||
requiresRestart: false,
|
||||
},
|
||||
],
|
||||
}) as never,
|
||||
});
|
||||
assert.deepEqual(getState(), [
|
||||
{
|
||||
id: 'anki.autoUpdateNewCards',
|
||||
label: 'X',
|
||||
scope: 'ankiConnect',
|
||||
valueType: 'boolean',
|
||||
value: true,
|
||||
allowedValues: [true, false],
|
||||
requiresRestart: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('restore previous secondary subtitle visibility no-ops without connected mpv client', () => {
|
||||
let restored = false;
|
||||
const restore = createRestorePreviousSecondarySubVisibilityHandler({
|
||||
getMpvClient: () => ({ connected: false, restorePreviousSecondarySubVisibility: () => (restored = true) }),
|
||||
});
|
||||
restore();
|
||||
assert.equal(restored, false);
|
||||
});
|
||||
|
||||
test('restore previous secondary subtitle visibility calls runtime when connected', () => {
|
||||
let restored = false;
|
||||
const restore = createRestorePreviousSecondarySubVisibilityHandler({
|
||||
getMpvClient: () => ({ connected: true, restorePreviousSecondarySubVisibility: () => (restored = true) }),
|
||||
});
|
||||
restore();
|
||||
assert.equal(restored, true);
|
||||
});
|
||||
|
||||
test('broadcast runtime options changed passes through state getter and broadcaster', () => {
|
||||
const calls: string[] = [];
|
||||
const broadcast = createBroadcastRuntimeOptionsChangedHandler({
|
||||
broadcastRuntimeOptionsChangedRuntime: (getState, emit) => {
|
||||
calls.push(`state:${JSON.stringify(getState())}`);
|
||||
emit('runtime-options:changed', { id: 1 });
|
||||
},
|
||||
getRuntimeOptionsState: () => [
|
||||
{
|
||||
id: 'anki.autoUpdateNewCards',
|
||||
label: 'X',
|
||||
scope: 'ankiConnect',
|
||||
valueType: 'boolean',
|
||||
value: true,
|
||||
allowedValues: [true, false],
|
||||
requiresRestart: false,
|
||||
},
|
||||
],
|
||||
broadcastToOverlayWindows: (channel, payload) => calls.push(`emit:${channel}:${JSON.stringify(payload)}`),
|
||||
});
|
||||
|
||||
broadcast();
|
||||
assert.deepEqual(calls, [
|
||||
'state:[{"id":"anki.autoUpdateNewCards","label":"X","scope":"ankiConnect","valueType":"boolean","value":true,"allowedValues":[true,false],"requiresRestart":false}]',
|
||||
'emit:runtime-options:changed:{"id":1}',
|
||||
]);
|
||||
});
|
||||
|
||||
test('send to active overlay window delegates to runtime sender', () => {
|
||||
const send = createSendToActiveOverlayWindowHandler({
|
||||
sendToActiveOverlayWindowRuntime: (channel, payload) => channel === 'ok' && payload === 1,
|
||||
});
|
||||
assert.equal(send('ok', 1), true);
|
||||
assert.equal(send('no', 1), false);
|
||||
});
|
||||
|
||||
test('set overlay debug visualization enabled delegates with current state and broadcast', () => {
|
||||
const calls: string[] = [];
|
||||
let current = false;
|
||||
const setEnabled = createSetOverlayDebugVisualizationEnabledHandler({
|
||||
setOverlayDebugVisualizationEnabledRuntime: (curr, next, setCurrent, broadcast) => {
|
||||
calls.push(`runtime:${curr}->${next}`);
|
||||
setCurrent(next);
|
||||
broadcast('overlay-debug:set', next);
|
||||
},
|
||||
getCurrentEnabled: () => current,
|
||||
setCurrentEnabled: (enabled) => {
|
||||
current = enabled;
|
||||
calls.push(`set:${enabled}`);
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, value) => calls.push(`emit:${channel}:${value}`),
|
||||
});
|
||||
|
||||
setEnabled(true);
|
||||
assert.equal(current, true);
|
||||
assert.deepEqual(calls, ['runtime:false->true', 'set:true', 'emit:overlay-debug:set:true']);
|
||||
});
|
||||
|
||||
test('open runtime options palette handler delegates to runtime', () => {
|
||||
let opened = false;
|
||||
const open = createOpenRuntimeOptionsPaletteHandler({
|
||||
openRuntimeOptionsPaletteRuntime: () => {
|
||||
opened = true;
|
||||
},
|
||||
});
|
||||
open();
|
||||
assert.equal(opened, true);
|
||||
});
|
||||
90
src/main/runtime/overlay-runtime-main-actions.ts
Normal file
90
src/main/runtime/overlay-runtime-main-actions.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { RuntimeOptionState } from '../../types';
|
||||
import type { OverlayHostedModal } from '../overlay-runtime';
|
||||
|
||||
type RuntimeOptionsManagerLike = {
|
||||
listOptions: () => RuntimeOptionState[];
|
||||
};
|
||||
|
||||
type MpvClientLike = {
|
||||
connected: boolean;
|
||||
restorePreviousSecondarySubVisibility: () => void;
|
||||
};
|
||||
|
||||
export function createGetRuntimeOptionsStateHandler(deps: {
|
||||
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
|
||||
}) {
|
||||
return (): RuntimeOptionState[] => {
|
||||
const manager = deps.getRuntimeOptionsManager();
|
||||
if (!manager) return [];
|
||||
return manager.listOptions();
|
||||
};
|
||||
}
|
||||
|
||||
export function createRestorePreviousSecondarySubVisibilityHandler(deps: {
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
}) {
|
||||
return (): void => {
|
||||
const client = deps.getMpvClient();
|
||||
if (!client || !client.connected) return;
|
||||
client.restorePreviousSecondarySubVisibility();
|
||||
};
|
||||
}
|
||||
|
||||
export function createBroadcastRuntimeOptionsChangedHandler(deps: {
|
||||
broadcastRuntimeOptionsChangedRuntime: (
|
||||
getRuntimeOptionsState: () => RuntimeOptionState[],
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||
) => void;
|
||||
getRuntimeOptionsState: () => RuntimeOptionState[];
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
deps.broadcastRuntimeOptionsChangedRuntime(
|
||||
() => deps.getRuntimeOptionsState(),
|
||||
(channel, ...args) => deps.broadcastToOverlayWindows(channel, ...args),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function createSendToActiveOverlayWindowHandler(deps: {
|
||||
sendToActiveOverlayWindowRuntime: (
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||
) => boolean;
|
||||
}) {
|
||||
return (
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||
): boolean => deps.sendToActiveOverlayWindowRuntime(channel, payload, runtimeOptions);
|
||||
}
|
||||
|
||||
export function createSetOverlayDebugVisualizationEnabledHandler(deps: {
|
||||
setOverlayDebugVisualizationEnabledRuntime: (
|
||||
currentEnabled: boolean,
|
||||
nextEnabled: boolean,
|
||||
setCurrentEnabled: (enabled: boolean) => void,
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||
) => void;
|
||||
getCurrentEnabled: () => boolean;
|
||||
setCurrentEnabled: (enabled: boolean) => void;
|
||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||
}) {
|
||||
return (enabled: boolean): void => {
|
||||
deps.setOverlayDebugVisualizationEnabledRuntime(
|
||||
deps.getCurrentEnabled(),
|
||||
enabled,
|
||||
(next) => deps.setCurrentEnabled(next),
|
||||
(channel, ...args) => deps.broadcastToOverlayWindows(channel, ...args),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function createOpenRuntimeOptionsPaletteHandler(deps: {
|
||||
openRuntimeOptionsPaletteRuntime: () => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
deps.openRuntimeOptionsPaletteRuntime();
|
||||
};
|
||||
}
|
||||
39
src/main/runtime/secondary-sub-mode-main-deps.test.ts
Normal file
39
src/main/runtime/secondary-sub-mode-main-deps.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildCycleSecondarySubModeMainDepsHandler } from './secondary-sub-mode-main-deps';
|
||||
import type { SecondarySubMode } from '../../types';
|
||||
|
||||
test('cycle secondary sub mode main deps builder maps state and broadcasts with channel', () => {
|
||||
const calls: string[] = [];
|
||||
let mode: SecondarySubMode = 'hover';
|
||||
let lastToggleAt = 100;
|
||||
const deps = createBuildCycleSecondarySubModeMainDepsHandler({
|
||||
getSecondarySubMode: () => mode,
|
||||
setSecondarySubMode: (nextMode) => {
|
||||
mode = nextMode;
|
||||
calls.push(`set-mode:${nextMode}`);
|
||||
},
|
||||
getLastSecondarySubToggleAtMs: () => lastToggleAt,
|
||||
setLastSecondarySubToggleAtMs: (timestampMs) => {
|
||||
lastToggleAt = timestampMs;
|
||||
calls.push(`set-ts:${timestampMs}`);
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, nextMode) => calls.push(`broadcast:${channel}:${nextMode}`),
|
||||
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||
})();
|
||||
|
||||
assert.equal(deps.getSecondarySubMode(), 'hover');
|
||||
assert.equal(deps.getLastSecondarySubToggleAtMs(), 100);
|
||||
deps.setSecondarySubMode('visible');
|
||||
deps.setLastSecondarySubToggleAtMs(200);
|
||||
deps.broadcastSecondarySubMode('visible');
|
||||
deps.showMpvOsd('Secondary subtitle: visible');
|
||||
assert.equal(mode, 'visible');
|
||||
assert.equal(lastToggleAt, 200);
|
||||
assert.deepEqual(calls, [
|
||||
'set-mode:visible',
|
||||
'set-ts:200',
|
||||
'broadcast:secondary-subtitle:mode:visible',
|
||||
'osd:Secondary subtitle: visible',
|
||||
]);
|
||||
});
|
||||
21
src/main/runtime/secondary-sub-mode-main-deps.ts
Normal file
21
src/main/runtime/secondary-sub-mode-main-deps.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { SecondarySubMode } from '../../types';
|
||||
|
||||
export function createBuildCycleSecondarySubModeMainDepsHandler(deps: {
|
||||
getSecondarySubMode: () => SecondarySubMode;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
getLastSecondarySubToggleAtMs: () => number;
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
|
||||
broadcastToOverlayWindows: (channel: string, mode: SecondarySubMode) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getSecondarySubMode: () => deps.getSecondarySubMode(),
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => deps.setSecondarySubMode(mode),
|
||||
getLastSecondarySubToggleAtMs: () => deps.getLastSecondarySubToggleAtMs(),
|
||||
setLastSecondarySubToggleAtMs: (timestampMs: number) =>
|
||||
deps.setLastSecondarySubToggleAtMs(timestampMs),
|
||||
broadcastSecondarySubMode: (mode: SecondarySubMode) =>
|
||||
deps.broadcastToOverlayWindows('secondary-subtitle:mode', mode),
|
||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||
});
|
||||
}
|
||||
52
src/main/runtime/startup-bootstrap-main-deps.test.ts
Normal file
52
src/main/runtime/startup-bootstrap-main-deps.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildStartupBootstrapMainDepsHandler } from './startup-bootstrap-main-deps';
|
||||
|
||||
test('startup bootstrap main deps builder maps deps and handles generate-config callbacks', () => {
|
||||
const calls: string[] = [];
|
||||
let exitCode = 0;
|
||||
const deps = createBuildStartupBootstrapMainDepsHandler({
|
||||
argv: ['node', 'main.js'],
|
||||
parseArgs: () => ({}) as never,
|
||||
setLogLevel: (level) => calls.push(`log:${level}`),
|
||||
forceX11Backend: () => calls.push('force-x11'),
|
||||
enforceUnsupportedWaylandMode: () => calls.push('guard-wayland'),
|
||||
shouldStartApp: () => true,
|
||||
getDefaultSocketPath: () => '/tmp/mpv.sock',
|
||||
defaultTexthookerPort: 5174,
|
||||
configDir: '/tmp/config',
|
||||
defaultConfig: {} as never,
|
||||
generateConfigTemplate: () => 'template',
|
||||
generateDefaultConfigFile: async () => 0,
|
||||
setExitCode: (code) => {
|
||||
exitCode = code;
|
||||
calls.push(`exit:${code}`);
|
||||
},
|
||||
quitApp: () => calls.push('quit'),
|
||||
logGenerateConfigError: (message) => calls.push(`error:${message}`),
|
||||
startAppLifecycle: () => calls.push('start-lifecycle'),
|
||||
})();
|
||||
|
||||
assert.deepEqual(deps.argv, ['node', 'main.js']);
|
||||
assert.equal(deps.getDefaultSocketPath(), '/tmp/mpv.sock');
|
||||
deps.setLogLevel('debug', 'config');
|
||||
deps.forceX11Backend({} as never);
|
||||
deps.enforceUnsupportedWaylandMode({} as never);
|
||||
deps.startAppLifecycle({} as never);
|
||||
deps.onConfigGenerated(7);
|
||||
assert.equal(exitCode, 7);
|
||||
deps.onGenerateConfigError(new Error('boom'));
|
||||
assert.equal(exitCode, 1);
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'log:debug',
|
||||
'force-x11',
|
||||
'guard-wayland',
|
||||
'start-lifecycle',
|
||||
'exit:7',
|
||||
'quit',
|
||||
'error:Failed to generate config: boom',
|
||||
'exit:1',
|
||||
'quit',
|
||||
]);
|
||||
});
|
||||
62
src/main/runtime/startup-bootstrap-main-deps.ts
Normal file
62
src/main/runtime/startup-bootstrap-main-deps.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { CliArgs } from '../../cli/args';
|
||||
import type { LogLevelSource } from '../../logger';
|
||||
import type { ResolvedConfig } from '../../types';
|
||||
import type { StartupBootstrapRuntimeFactoryDeps } from '../startup';
|
||||
|
||||
export function createBuildStartupBootstrapMainDepsHandler(deps: {
|
||||
argv: string[];
|
||||
parseArgs: (argv: string[]) => CliArgs;
|
||||
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||
forceX11Backend: (args: CliArgs) => void;
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||
shouldStartApp: (args: CliArgs) => boolean;
|
||||
getDefaultSocketPath: () => string;
|
||||
defaultTexthookerPort: number;
|
||||
configDir: string;
|
||||
defaultConfig: ResolvedConfig;
|
||||
generateConfigTemplate: (config: ResolvedConfig) => string;
|
||||
generateDefaultConfigFile: (
|
||||
args: CliArgs,
|
||||
options: {
|
||||
configDir: string;
|
||||
defaultConfig: unknown;
|
||||
generateTemplate: (config: unknown) => string;
|
||||
},
|
||||
) => Promise<number>;
|
||||
setExitCode: (code: number) => void;
|
||||
quitApp: () => void;
|
||||
logGenerateConfigError: (message: string) => void;
|
||||
startAppLifecycle: (args: CliArgs) => void;
|
||||
}) {
|
||||
return (): StartupBootstrapRuntimeFactoryDeps => ({
|
||||
argv: deps.argv,
|
||||
parseArgs: (argv: string[]) => deps.parseArgs(argv),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => deps.setLogLevel(level, source),
|
||||
forceX11Backend: (args: CliArgs) => deps.forceX11Backend(args),
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => deps.enforceUnsupportedWaylandMode(args),
|
||||
shouldStartApp: (args: CliArgs) => deps.shouldStartApp(args),
|
||||
getDefaultSocketPath: () => deps.getDefaultSocketPath(),
|
||||
defaultTexthookerPort: deps.defaultTexthookerPort,
|
||||
configDir: deps.configDir,
|
||||
defaultConfig: deps.defaultConfig,
|
||||
generateConfigTemplate: (config: ResolvedConfig) => deps.generateConfigTemplate(config),
|
||||
generateDefaultConfigFile: (
|
||||
args: CliArgs,
|
||||
options: {
|
||||
configDir: string;
|
||||
defaultConfig: unknown;
|
||||
generateTemplate: (config: unknown) => string;
|
||||
},
|
||||
) => deps.generateDefaultConfigFile(args, options),
|
||||
onConfigGenerated: (exitCode: number) => {
|
||||
deps.setExitCode(exitCode);
|
||||
deps.quitApp();
|
||||
},
|
||||
onGenerateConfigError: (error: Error) => {
|
||||
deps.logGenerateConfigError(`Failed to generate config: ${error.message}`);
|
||||
deps.setExitCode(1);
|
||||
deps.quitApp();
|
||||
},
|
||||
startAppLifecycle: (args: CliArgs) => deps.startAppLifecycle(args),
|
||||
});
|
||||
}
|
||||
64
src/main/runtime/startup-config-main-deps.test.ts
Normal file
64
src/main/runtime/startup-config-main-deps.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createBuildCriticalConfigErrorMainDepsHandler,
|
||||
createBuildReloadConfigMainDepsHandler,
|
||||
} from './startup-config-main-deps';
|
||||
|
||||
test('reload config main deps builder maps callbacks and fail handlers', async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildReloadConfigMainDepsHandler({
|
||||
reloadConfigStrict: () => ({ ok: true }),
|
||||
logInfo: (message) => calls.push(`info:${message}`),
|
||||
logWarning: (message) => calls.push(`warn:${message}`),
|
||||
showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`),
|
||||
startConfigHotReload: () => calls.push('start-hot-reload'),
|
||||
refreshAnilistClientSecretState: async (options) => {
|
||||
calls.push(`refresh:${options.force}`);
|
||||
return true;
|
||||
},
|
||||
failHandlers: {
|
||||
logError: (details) => calls.push(`error:${details}`),
|
||||
showErrorBox: (title, details) => calls.push(`error-box:${title}:${details}`),
|
||||
quit: () => calls.push('quit'),
|
||||
},
|
||||
})();
|
||||
|
||||
assert.deepEqual(deps.reloadConfigStrict(), { ok: true });
|
||||
deps.logInfo('x');
|
||||
deps.logWarning('y');
|
||||
deps.showDesktopNotification('SubMiner', { body: 'warn' });
|
||||
deps.startConfigHotReload();
|
||||
await deps.refreshAnilistClientSecretState({ force: true });
|
||||
deps.failHandlers.logError('bad');
|
||||
deps.failHandlers.showErrorBox('Oops', 'Details');
|
||||
deps.failHandlers.quit();
|
||||
assert.deepEqual(calls, [
|
||||
'info:x',
|
||||
'warn:y',
|
||||
'notify:SubMiner:warn',
|
||||
'start-hot-reload',
|
||||
'refresh:true',
|
||||
'error:bad',
|
||||
'error-box:Oops:Details',
|
||||
'quit',
|
||||
]);
|
||||
});
|
||||
|
||||
test('critical config main deps builder maps config path and fail handlers', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildCriticalConfigErrorMainDepsHandler({
|
||||
getConfigPath: () => '/tmp/config.jsonc',
|
||||
failHandlers: {
|
||||
logError: (details) => calls.push(`error:${details}`),
|
||||
showErrorBox: (title, details) => calls.push(`error-box:${title}:${details}`),
|
||||
quit: () => calls.push('quit'),
|
||||
},
|
||||
})();
|
||||
|
||||
assert.equal(deps.getConfigPath(), '/tmp/config.jsonc');
|
||||
deps.failHandlers.logError('bad');
|
||||
deps.failHandlers.showErrorBox('Oops', 'Details');
|
||||
deps.failHandlers.quit();
|
||||
assert.deepEqual(calls, ['error:bad', 'error-box:Oops:Details', 'quit']);
|
||||
});
|
||||
47
src/main/runtime/startup-config-main-deps.ts
Normal file
47
src/main/runtime/startup-config-main-deps.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export function createBuildReloadConfigMainDepsHandler(deps: {
|
||||
reloadConfigStrict: () => unknown;
|
||||
logInfo: (message: string) => void;
|
||||
logWarning: (message: string) => void;
|
||||
showDesktopNotification: (title: string, options: { body: string }) => void;
|
||||
startConfigHotReload: () => void;
|
||||
refreshAnilistClientSecretState: (options: { force: boolean }) => Promise<unknown>;
|
||||
failHandlers: {
|
||||
logError: (details: string) => void;
|
||||
showErrorBox: (title: string, details: string) => void;
|
||||
quit: () => void;
|
||||
};
|
||||
}) {
|
||||
return () => ({
|
||||
reloadConfigStrict: () => deps.reloadConfigStrict() as never,
|
||||
logInfo: (message: string) => deps.logInfo(message),
|
||||
logWarning: (message: string) => deps.logWarning(message),
|
||||
showDesktopNotification: (title: string, options: { body: string }) =>
|
||||
deps.showDesktopNotification(title, options),
|
||||
startConfigHotReload: () => deps.startConfigHotReload(),
|
||||
refreshAnilistClientSecretState: (options: { force: boolean }) =>
|
||||
deps.refreshAnilistClientSecretState(options),
|
||||
failHandlers: {
|
||||
logError: (details: string) => deps.failHandlers.logError(details),
|
||||
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||
quit: () => deps.failHandlers.quit(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildCriticalConfigErrorMainDepsHandler(deps: {
|
||||
getConfigPath: () => string;
|
||||
failHandlers: {
|
||||
logError: (details: string) => void;
|
||||
showErrorBox: (title: string, details: string) => void;
|
||||
quit: () => void;
|
||||
};
|
||||
}) {
|
||||
return () => ({
|
||||
getConfigPath: () => deps.getConfigPath(),
|
||||
failHandlers: {
|
||||
logError: (details: string) => deps.failHandlers.logError(details),
|
||||
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||
quit: () => deps.failHandlers.quit(),
|
||||
},
|
||||
});
|
||||
}
|
||||
35
src/main/runtime/startup-lifecycle-main-deps.test.ts
Normal file
35
src/main/runtime/startup-lifecycle-main-deps.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildAppLifecycleRuntimeRunnerMainDepsHandler } from './startup-lifecycle-main-deps';
|
||||
|
||||
test('app lifecycle runtime runner main deps builder maps lifecycle callbacks', async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildAppLifecycleRuntimeRunnerMainDepsHandler({
|
||||
app: {} as never,
|
||||
platform: 'darwin',
|
||||
shouldStartApp: () => true,
|
||||
parseArgs: () => ({}) as never,
|
||||
handleCliCommand: () => calls.push('handle-cli'),
|
||||
printHelp: () => calls.push('help'),
|
||||
logNoRunningInstance: () => calls.push('no-instance'),
|
||||
onReady: async () => {
|
||||
calls.push('ready');
|
||||
},
|
||||
onWillQuitCleanup: () => calls.push('cleanup'),
|
||||
shouldRestoreWindowsOnActivate: () => true,
|
||||
restoreWindowsOnActivate: () => calls.push('restore'),
|
||||
shouldQuitOnWindowAllClosed: () => false,
|
||||
})();
|
||||
|
||||
assert.equal(deps.platform, 'darwin');
|
||||
assert.equal(deps.shouldStartApp({} as never), true);
|
||||
deps.handleCliCommand({} as never, 'initial');
|
||||
deps.printHelp();
|
||||
deps.logNoRunningInstance();
|
||||
await deps.onReady();
|
||||
deps.onWillQuitCleanup();
|
||||
deps.restoreWindowsOnActivate();
|
||||
assert.equal(deps.shouldRestoreWindowsOnActivate(), true);
|
||||
assert.equal(deps.shouldQuitOnWindowAllClosed(), false);
|
||||
assert.deepEqual(calls, ['handle-cli', 'help', 'no-instance', 'ready', 'cleanup', 'restore']);
|
||||
});
|
||||
20
src/main/runtime/startup-lifecycle-main-deps.ts
Normal file
20
src/main/runtime/startup-lifecycle-main-deps.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { AppLifecycleRuntimeRunnerParams } from '../startup-lifecycle';
|
||||
|
||||
export function createBuildAppLifecycleRuntimeRunnerMainDepsHandler(
|
||||
deps: AppLifecycleRuntimeRunnerParams,
|
||||
) {
|
||||
return (): AppLifecycleRuntimeRunnerParams => ({
|
||||
app: deps.app,
|
||||
platform: deps.platform,
|
||||
shouldStartApp: deps.shouldStartApp,
|
||||
parseArgs: deps.parseArgs,
|
||||
handleCliCommand: deps.handleCliCommand,
|
||||
printHelp: deps.printHelp,
|
||||
logNoRunningInstance: deps.logNoRunningInstance,
|
||||
onReady: deps.onReady,
|
||||
onWillQuitCleanup: deps.onWillQuitCleanup,
|
||||
shouldRestoreWindowsOnActivate: deps.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: deps.restoreWindowsOnActivate,
|
||||
shouldQuitOnWindowAllClosed: deps.shouldQuitOnWindowAllClosed,
|
||||
});
|
||||
}
|
||||
77
src/main/runtime/subtitle-tokenization-main-deps.test.ts
Normal file
77
src/main/runtime/subtitle-tokenization-main-deps.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createBuildTokenizerDepsMainHandler,
|
||||
createCreateMecabTokenizerAndCheckMainHandler,
|
||||
createPrewarmSubtitleDictionariesMainHandler,
|
||||
} from './subtitle-tokenization-main-deps';
|
||||
|
||||
test('tokenizer deps builder records known-word lookups and maps readers', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildTokenizerDepsMainHandler({
|
||||
getYomitanExt: () => ({ id: 'ext' }),
|
||||
getYomitanParserWindow: () => ({ id: 'window' }),
|
||||
setYomitanParserWindow: () => calls.push('set-window'),
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => calls.push('set-ready'),
|
||||
getYomitanParserInitPromise: () => null,
|
||||
setYomitanParserInitPromise: () => calls.push('set-init'),
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: (hit) => calls.push(`lookup:${hit}`),
|
||||
getKnownWordMatchMode: () => 'exact',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => 'N2',
|
||||
getJlptEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyRank: () => 5,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
})();
|
||||
|
||||
assert.equal(deps.isKnownWord('known'), true);
|
||||
assert.equal(deps.isKnownWord('unknown'), false);
|
||||
deps.setYomitanParserWindow({});
|
||||
deps.setYomitanParserReadyPromise(null);
|
||||
deps.setYomitanParserInitPromise(null);
|
||||
assert.equal(deps.getMinSentenceWordsForNPlusOne(), 3);
|
||||
assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']);
|
||||
});
|
||||
|
||||
test('mecab tokenizer check creates tokenizer once and runs availability check', async () => {
|
||||
const calls: string[] = [];
|
||||
type Tokenizer = { id: string };
|
||||
let tokenizer: Tokenizer | null = null;
|
||||
const run = createCreateMecabTokenizerAndCheckMainHandler<Tokenizer>({
|
||||
getMecabTokenizer: () => tokenizer,
|
||||
setMecabTokenizer: (next) => {
|
||||
tokenizer = next;
|
||||
calls.push('set');
|
||||
},
|
||||
createMecabTokenizer: () => {
|
||||
calls.push('create');
|
||||
return { id: 'mecab' };
|
||||
},
|
||||
checkAvailability: async () => {
|
||||
calls.push('check');
|
||||
},
|
||||
});
|
||||
|
||||
await run();
|
||||
await run();
|
||||
assert.deepEqual(calls, ['create', 'set', 'check', 'check']);
|
||||
});
|
||||
|
||||
test('dictionary prewarm runs both dictionary loaders', async () => {
|
||||
const calls: string[] = [];
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
ensureJlptDictionaryLookup: async () => {
|
||||
calls.push('jlpt');
|
||||
},
|
||||
ensureFrequencyDictionaryLookup: async () => {
|
||||
calls.push('freq');
|
||||
},
|
||||
});
|
||||
|
||||
await prewarm();
|
||||
assert.deepEqual(calls.sort(), ['freq', 'jlpt']);
|
||||
});
|
||||
69
src/main/runtime/subtitle-tokenization-main-deps.ts
Normal file
69
src/main/runtime/subtitle-tokenization-main-deps.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export function createBuildTokenizerDepsMainHandler(deps: {
|
||||
getYomitanExt: () => unknown;
|
||||
getYomitanParserWindow: () => unknown;
|
||||
setYomitanParserWindow: (window: unknown) => void;
|
||||
getYomitanParserReadyPromise: () => Promise<void> | null;
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||
getYomitanParserInitPromise: () => Promise<boolean> | null;
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
isKnownWord: (text: string) => boolean;
|
||||
recordLookup: (hit: boolean) => void;
|
||||
getKnownWordMatchMode: () => unknown;
|
||||
getMinSentenceWordsForNPlusOne: () => number;
|
||||
getJlptLevel: (text: string) => unknown;
|
||||
getJlptEnabled: () => boolean;
|
||||
getFrequencyDictionaryEnabled: () => boolean;
|
||||
getFrequencyRank: (text: string) => unknown;
|
||||
getYomitanGroupDebugEnabled: () => boolean;
|
||||
getMecabTokenizer: () => unknown;
|
||||
}) {
|
||||
return () => ({
|
||||
getYomitanExt: () => deps.getYomitanExt() as never,
|
||||
getYomitanParserWindow: () => deps.getYomitanParserWindow() as never,
|
||||
setYomitanParserWindow: (window: unknown) => deps.setYomitanParserWindow(window),
|
||||
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise() as never,
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) =>
|
||||
deps.setYomitanParserReadyPromise(promise),
|
||||
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise() as never,
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) =>
|
||||
deps.setYomitanParserInitPromise(promise),
|
||||
isKnownWord: (text: string) => {
|
||||
const hit = deps.isKnownWord(text);
|
||||
deps.recordLookup(hit);
|
||||
return hit;
|
||||
},
|
||||
getKnownWordMatchMode: () => deps.getKnownWordMatchMode() as never,
|
||||
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
|
||||
getJlptLevel: (text: string) => deps.getJlptLevel(text) as never,
|
||||
getJlptEnabled: () => deps.getJlptEnabled(),
|
||||
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text) as never,
|
||||
getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(),
|
||||
getMecabTokenizer: () => deps.getMecabTokenizer() as never,
|
||||
});
|
||||
}
|
||||
|
||||
export function createCreateMecabTokenizerAndCheckMainHandler<TMecab>(deps: {
|
||||
getMecabTokenizer: () => TMecab | null;
|
||||
setMecabTokenizer: (tokenizer: TMecab) => void;
|
||||
createMecabTokenizer: () => TMecab;
|
||||
checkAvailability: (tokenizer: TMecab) => Promise<unknown>;
|
||||
}) {
|
||||
return async (): Promise<void> => {
|
||||
let tokenizer = deps.getMecabTokenizer();
|
||||
if (!tokenizer) {
|
||||
tokenizer = deps.createMecabTokenizer();
|
||||
deps.setMecabTokenizer(tokenizer);
|
||||
}
|
||||
await deps.checkAvailability(tokenizer);
|
||||
};
|
||||
}
|
||||
|
||||
export function createPrewarmSubtitleDictionariesMainHandler(deps: {
|
||||
ensureJlptDictionaryLookup: () => Promise<void>;
|
||||
ensureFrequencyDictionaryLookup: () => Promise<void>;
|
||||
}) {
|
||||
return async (): Promise<void> => {
|
||||
await Promise.all([deps.ensureJlptDictionaryLookup(), deps.ensureFrequencyDictionaryLookup()]);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user