mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 04:46:07 -08:00
refactor(main): fold remaining main-deps builders into their handlers (#308)
* refactor(main): drop remaining pass-through main-deps builders Jellyfin client-info/setup-window/remote, overlay shortcuts/visibility/ layout/window-factory, config hot-reload, and startup bootstrap factory builders only forwarded deps. Callers now pass deps to the handlers directly; createWatchConfigPathHandler moves into config-hot-reload-handlers. * refactor(main): fold small-logic main-deps builders into their handlers Tray click, mpv client factory, startup bootstrap exit callbacks, overlay runtime options, and dictionary roots/runtime builders move their logic into the owning handler or call site. The two duplicated dictionary-root builders become one createBuildDictionaryRootsHandler(vendorDirName). * refactor(main): fold cli command context builders into the factory createCliCommandContextFactory now adapts appState and main services to the CLI command context deps directly (socket-path change notification, texthooker port/websocket URL, browser-open default). The pass-through cli-command-context-main-deps and cli-command-context-deps layers are removed; their behavioural tests now run against the factory's context. * refactor(main): fold subtitle tokenization builder into the mpv composer The tokenizer deps builder's logic (known-word lookup recording and the character-dictionary gate on name matching) becomes buildTokenizerRuntimeOptions in mpv-runtime-composer, tested through the composer. The MeCab check and dictionary prewarm handlers move to subtitle-tokenization-warmup.ts, and the unused BuiltMainDeps type is removed from composer contracts. * refactor(main): drop redundant tokenizer deps keys and cast * refactor(main): end the main-deps builder pattern mpv-main-event-main-deps becomes mpv-main-event-binding-deps (createMpvMainEventBindingDeps), returning the binding deps directly instead of a thunk; it owns the subtitle-recording state. The will-quit cleanup adapter likewise returns its deps directly, and the pass-through app-lifecycle activate builders are removed. * docs: drop references to main-deps builders
This commit is contained in:
1 parent
2a0e931325
commit
90fc7cf940
63 files changed
+1282
-2480
No files matched your search
@@ -1,21 +0,0 @@
|
||||
export function createBuildShouldRestoreWindowsOnActivateMainDepsHandler(deps: {
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
getAllWindowCount: () => number;
|
||||
}) {
|
||||
return () => ({
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
getAllWindowCount: () => deps.getAllWindowCount(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildRestoreWindowsOnActivateMainDepsHandler(deps: {
|
||||
createMainWindow: () => void;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
syncOverlayMpvSubtitleSuppression: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createMainWindow: () => deps.createMainWindow(),
|
||||
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(),
|
||||
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildOnWillQuitCleanupDepsHandler } from './app-lifecycle-main-cleanup';
|
||||
import { createOnWillQuitCleanupDeps } from './app-lifecycle-main-cleanup';
|
||||
import { createOnWillQuitCleanupHandler } from './app-lifecycle-actions';
|
||||
|
||||
type BuilderDeps = Parameters<typeof createBuildOnWillQuitCleanupDepsHandler>[0];
|
||||
type BuilderDeps = Parameters<typeof createOnWillQuitCleanupDeps>[0];
|
||||
|
||||
// Defaults model an app that is ready but has no live runtime objects.
|
||||
function buildCleanupDeps(overrides: Partial<BuilderDeps> = {}) {
|
||||
const noop = () => {};
|
||||
const none = () => null;
|
||||
return createBuildOnWillQuitCleanupDepsHandler({
|
||||
return createOnWillQuitCleanupDeps({
|
||||
destroyTray: noop,
|
||||
stopConfigHotReload: noop,
|
||||
restorePreviousSecondarySubVisibility: noop,
|
||||
@@ -53,7 +53,7 @@ function buildCleanupDeps(overrides: Partial<BuilderDeps> = {}) {
|
||||
cleanupJellyfinSubtitleCache: noop,
|
||||
stopDiscordPresenceService: noop,
|
||||
...overrides,
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
function fakeWindow(calls: string[], destroyed: boolean) {
|
||||
|
||||
@@ -17,7 +17,9 @@ type SocketLike = {
|
||||
|
||||
type TimerLike = ReturnType<typeof setTimeout>;
|
||||
|
||||
export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
||||
// Adapts main-process state to the will-quit cleanup steps: each destroy/stop step looks up
|
||||
// its current resource and skips it when absent or already destroyed.
|
||||
export function createOnWillQuitCleanupDeps(deps: {
|
||||
destroyTray: () => void;
|
||||
stopConfigHotReload: () => void;
|
||||
restorePreviousSecondarySubVisibility: () => void;
|
||||
@@ -66,7 +68,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
||||
cleanupJellyfinSubtitleCache: () => void;
|
||||
stopDiscordPresenceService: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
return {
|
||||
destroyTray: () => deps.destroyTray(),
|
||||
stopConfigHotReload: () => deps.stopConfigHotReload(),
|
||||
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
|
||||
@@ -154,5 +156,5 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
||||
cleanupRemoteMediaWindows: () => deps.cleanupRemoteMediaWindows(),
|
||||
cleanupJellyfinSubtitleCache: () => deps.cleanupJellyfinSubtitleCache(),
|
||||
stopDiscordPresenceService: () => deps.stopDiscordPresenceService(),
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildEnsureTrayMainDepsHandler } from './app-runtime-main-deps';
|
||||
|
||||
test('ensure tray main deps trigger overlay bootstrap on tray click when runtime not initialized', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildEnsureTrayMainDepsHandler({
|
||||
getTray: () => null,
|
||||
setTray: () => calls.push('set-tray'),
|
||||
buildTrayMenu: () => ({}),
|
||||
resolveTrayIconPath: () => null,
|
||||
createImageFromPath: () => ({}),
|
||||
createEmptyImage: () => ({}),
|
||||
createTray: () => ({}),
|
||||
trayTooltip: 'SubMiner',
|
||||
platform: 'darwin',
|
||||
logWarn: (message) => calls.push(`warn:${message}`),
|
||||
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),
|
||||
})();
|
||||
|
||||
deps.ensureOverlayVisibleFromTrayClick();
|
||||
assert.deepEqual(calls, ['init-overlay', 'set-visible:true']);
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
export function createBuildEnsureTrayMainDepsHandler<TTray, TTrayMenu, TTrayIcon>(deps: {
|
||||
getTray: () => TTray | null;
|
||||
setTray: (tray: TTray | null) => void;
|
||||
buildTrayMenu: () => TTrayMenu;
|
||||
resolveTrayIconPath: () => string | null;
|
||||
createImageFromPath: (iconPath: string) => TTrayIcon;
|
||||
createEmptyImage: () => TTrayIcon;
|
||||
createTray: (icon: TTrayIcon) => TTray;
|
||||
trayTooltip: string;
|
||||
platform: string;
|
||||
logWarn: (message: string) => void;
|
||||
initializeOverlayRuntime: () => void;
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getTray: () => deps.getTray(),
|
||||
setTray: (tray: TTray | null) => deps.setTray(tray),
|
||||
buildTrayMenu: () => deps.buildTrayMenu(),
|
||||
resolveTrayIconPath: () => deps.resolveTrayIconPath(),
|
||||
createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath),
|
||||
createEmptyImage: () => deps.createEmptyImage(),
|
||||
createTray: (icon: TTrayIcon) => deps.createTray(icon),
|
||||
trayTooltip: deps.trayTooltip,
|
||||
platform: deps.platform,
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
ensureOverlayVisibleFromTrayClick: () => {
|
||||
if (!deps.isOverlayRuntimeInitialized()) {
|
||||
deps.initializeOverlayRuntime();
|
||||
}
|
||||
deps.setVisibleOverlayVisible(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildDestroyTrayMainDepsHandler<TTray>(deps: {
|
||||
getTray: () => TTray | null;
|
||||
setTray: (tray: TTray | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getTray: () => deps.getTray(),
|
||||
setTray: (tray: TTray | null) => deps.setTray(tray),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOptions>(deps: {
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => void;
|
||||
buildOptions: () => TOptions;
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) => void;
|
||||
startBackgroundWarmups: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options),
|
||||
buildOptions: () => deps.buildOptions(),
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) =>
|
||||
deps.setOverlayRuntimeInitialized(initialized),
|
||||
startBackgroundWarmups: () => deps.startBackgroundWarmups(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWindow>(deps: {
|
||||
ensureYomitanExtensionLoaded: () => Promise<TYomitanExt | null>;
|
||||
getYomitanExtension?: () => TYomitanExt | null;
|
||||
getYomitanExtensionLoadInFlight?: () => Promise<unknown> | null;
|
||||
openYomitanSettingsWindow: (params: {
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
yomitanSession?: unknown | null;
|
||||
onWindowClosed?: () => void;
|
||||
}) => void;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
getYomitanSession?: () => unknown | null;
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, error: unknown) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
|
||||
...(deps.getYomitanExtension
|
||||
? { getYomitanExtension: () => deps.getYomitanExtension?.() ?? null }
|
||||
: {}),
|
||||
...(deps.getYomitanExtensionLoadInFlight
|
||||
? {
|
||||
getYomitanExtensionLoadInFlight: () => deps.getYomitanExtensionLoadInFlight?.() ?? null,
|
||||
}
|
||||
: {}),
|
||||
openYomitanSettingsWindow: (params: {
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
yomitanSession?: unknown | null;
|
||||
onWindowClosed?: () => void;
|
||||
}) => deps.openYomitanSettingsWindow(params),
|
||||
getExistingWindow: () => deps.getExistingWindow(),
|
||||
setWindow: (window: TWindow | null) => deps.setWindow(window),
|
||||
getYomitanSession: () => deps.getYomitanSession?.() ?? null,
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
logError: (message: string, error: unknown) => deps.logError(message, error),
|
||||
});
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import type { CliArgs } from '../../cli/args';
|
||||
import type { CliCommandContextFactoryDeps } from './cli-command-context';
|
||||
|
||||
export function createBuildCliCommandContextDepsHandler(deps: {
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
getSocketPath: () => string;
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
getMpvClient: CliCommandContextFactoryDeps['getMpvClient'];
|
||||
showOsd: (text: string) => void;
|
||||
showPlaybackFeedback?: (text: string) => void;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getTexthookerPort: () => number;
|
||||
setTexthookerPort: (port: number) => void;
|
||||
getTexthookerWebsocketUrl: () => string | undefined;
|
||||
shouldOpenBrowser: () => boolean;
|
||||
openExternal: (url: string) => Promise<unknown>;
|
||||
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||
isOverlayInitialized: () => boolean;
|
||||
initializeOverlay: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
togglePrimarySubtitleBar: () => void;
|
||||
openFirstRunSetup: (force?: boolean) => void;
|
||||
setVisibleOverlay: (visible: boolean) => void;
|
||||
copyCurrentSubtitle: () => void;
|
||||
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWordCache: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
dispatchSessionAction: CliCommandContextFactoryDeps['dispatchSessionAction'];
|
||||
getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
|
||||
clearAnilistToken: CliCommandContextFactoryDeps['clearAnilistToken'];
|
||||
openAnilistSetup: CliCommandContextFactoryDeps['openAnilistSetup'];
|
||||
openJellyfinSetup: CliCommandContextFactoryDeps['openJellyfinSetup'];
|
||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||
retryAnilistQueueNow: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
|
||||
getCharacterDictionarySelection?: CliCommandContextFactoryDeps['getCharacterDictionarySelection'];
|
||||
setCharacterDictionarySelection?: CliCommandContextFactoryDeps['setCharacterDictionarySelection'];
|
||||
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
runUpdateCommand: CliCommandContextFactoryDeps['runUpdateCommand'];
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: CliCommandContextFactoryDeps['runEnsureLinuxRuntimePluginAssetsCommand'];
|
||||
runYoutubePlaybackFlow: CliCommandContextFactoryDeps['runYoutubePlaybackFlow'];
|
||||
ensureBackgroundStatsServer?: CliCommandContextFactoryDeps['ensureBackgroundStatsServer'];
|
||||
openYomitanSettings: () => void;
|
||||
openHachidoriSettings: () => void;
|
||||
openConfigSettingsWindow: () => void;
|
||||
openSyncUiWindow: () => void;
|
||||
openYoutubeBrowserWindow: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
printHelp: () => void;
|
||||
stopApp: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
schedule: (fn: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
||||
logInfo: (message: string) => void;
|
||||
logDebug: (message: string) => void;
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
}) {
|
||||
return (): CliCommandContextFactoryDeps => ({
|
||||
setLogLevel: deps.setLogLevel,
|
||||
getSocketPath: deps.getSocketPath,
|
||||
setSocketPath: deps.setSocketPath,
|
||||
getMpvClient: deps.getMpvClient,
|
||||
showOsd: deps.showOsd,
|
||||
showPlaybackFeedback: deps.showPlaybackFeedback,
|
||||
texthookerService: deps.texthookerService,
|
||||
getTexthookerPort: deps.getTexthookerPort,
|
||||
setTexthookerPort: deps.setTexthookerPort,
|
||||
getTexthookerWebsocketUrl: deps.getTexthookerWebsocketUrl,
|
||||
shouldOpenBrowser: deps.shouldOpenBrowser,
|
||||
openExternal: deps.openExternal,
|
||||
logBrowserOpenError: deps.logBrowserOpenError,
|
||||
isOverlayInitialized: deps.isOverlayInitialized,
|
||||
initializeOverlay: deps.initializeOverlay,
|
||||
toggleVisibleOverlay: deps.toggleVisibleOverlay,
|
||||
togglePrimarySubtitleBar: deps.togglePrimarySubtitleBar,
|
||||
openFirstRunSetup: deps.openFirstRunSetup,
|
||||
setVisibleOverlay: deps.setVisibleOverlay,
|
||||
copyCurrentSubtitle: deps.copyCurrentSubtitle,
|
||||
startPendingMultiCopy: deps.startPendingMultiCopy,
|
||||
mineSentenceCard: deps.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple: deps.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: deps.updateLastCardFromClipboard,
|
||||
refreshKnownWordCache: deps.refreshKnownWordCache,
|
||||
triggerFieldGrouping: deps.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: deps.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: deps.markLastCardAsAudioCard,
|
||||
dispatchSessionAction: deps.dispatchSessionAction,
|
||||
getAnilistStatus: deps.getAnilistStatus,
|
||||
clearAnilistToken: deps.clearAnilistToken,
|
||||
openAnilistSetup: deps.openAnilistSetup,
|
||||
openJellyfinSetup: deps.openJellyfinSetup,
|
||||
getAnilistQueueStatus: deps.getAnilistQueueStatus,
|
||||
retryAnilistQueueNow: deps.retryAnilistQueueNow,
|
||||
generateCharacterDictionary: deps.generateCharacterDictionary,
|
||||
getCharacterDictionarySelection: deps.getCharacterDictionarySelection,
|
||||
setCharacterDictionarySelection: deps.setCharacterDictionarySelection,
|
||||
runStatsCommand: deps.runStatsCommand,
|
||||
runJellyfinCommand: deps.runJellyfinCommand,
|
||||
runUpdateCommand: deps.runUpdateCommand,
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: deps.runEnsureLinuxRuntimePluginAssetsCommand,
|
||||
runYoutubePlaybackFlow: deps.runYoutubePlaybackFlow,
|
||||
ensureBackgroundStatsServer: deps.ensureBackgroundStatsServer,
|
||||
openYomitanSettings: deps.openYomitanSettings,
|
||||
openHachidoriSettings: deps.openHachidoriSettings,
|
||||
openConfigSettingsWindow: deps.openConfigSettingsWindow,
|
||||
openSyncUiWindow: deps.openSyncUiWindow,
|
||||
openYoutubeBrowserWindow: deps.openYoutubeBrowserWindow,
|
||||
cycleSecondarySubMode: deps.cycleSecondarySubMode,
|
||||
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,
|
||||
printHelp: deps.printHelp,
|
||||
stopApp: deps.stopApp,
|
||||
hasMainWindow: deps.hasMainWindow,
|
||||
getMultiCopyTimeoutMs: deps.getMultiCopyTimeoutMs,
|
||||
schedule: deps.schedule,
|
||||
logInfo: deps.logInfo,
|
||||
logDebug: deps.logDebug,
|
||||
logWarn: deps.logWarn,
|
||||
logError: deps.logError,
|
||||
});
|
||||
}
|
||||
+14
-13
@@ -1,13 +1,14 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps';
|
||||
import {
|
||||
createCliCommandContextFactory,
|
||||
type CliCommandContextMainDeps,
|
||||
} from './cli-command-context-factory';
|
||||
|
||||
type MainDeps = Parameters<typeof createBuildCliCommandContextMainDepsHandler>[0];
|
||||
|
||||
function buildContextDeps(overrides: Partial<MainDeps> = {}) {
|
||||
function buildContext(overrides: Partial<CliCommandContextMainDeps> = {}) {
|
||||
const noop = () => {};
|
||||
const noopAsync = async () => {};
|
||||
return createBuildCliCommandContextMainDepsHandler({
|
||||
return createCliCommandContextFactory({
|
||||
appState: {
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
mpvClient: null,
|
||||
@@ -100,16 +101,16 @@ test('cli command context setSocketPath notifies only when the socket path chang
|
||||
texthookerPort: 5174,
|
||||
overlayRuntimeInitialized: false,
|
||||
};
|
||||
const deps = buildContextDeps({
|
||||
const context = buildContext({
|
||||
appState,
|
||||
onMpvSocketPathChanged: (next, previous) => changes.push(`${previous}->${next}`),
|
||||
});
|
||||
|
||||
deps.setSocketPath('/tmp/next.sock');
|
||||
deps.setSocketPath('/tmp/next.sock');
|
||||
context.setSocketPath('/tmp/next.sock');
|
||||
context.setSocketPath('/tmp/next.sock');
|
||||
|
||||
assert.equal(appState.mpvSocketPath, '/tmp/next.sock');
|
||||
assert.equal(deps.getSocketPath(), '/tmp/next.sock');
|
||||
assert.equal(context.getSocketPath(), '/tmp/next.sock');
|
||||
assert.deepEqual(changes, ['/tmp/mpv.sock->/tmp/next.sock']);
|
||||
});
|
||||
|
||||
@@ -119,10 +120,10 @@ for (const c of [
|
||||
{ openBrowser: false, expected: false },
|
||||
]) {
|
||||
test(`cli command context shouldOpenBrowser is ${c.expected} when texthooker.openBrowser is ${c.openBrowser}`, () => {
|
||||
const deps = buildContextDeps({
|
||||
const context = buildContext({
|
||||
getResolvedConfig: () => ({ texthooker: { openBrowser: c.openBrowser } }),
|
||||
});
|
||||
assert.equal(deps.shouldOpenBrowser(), c.expected);
|
||||
assert.equal(context.shouldOpenBrowser(), c.expected);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,10 +148,10 @@ for (const c of [
|
||||
},
|
||||
]) {
|
||||
test(`cli command context texthooker websocket url ${c.name}`, () => {
|
||||
const deps = buildContextDeps({
|
||||
const context = buildContext({
|
||||
getResolvedConfig: () => c.config,
|
||||
hasMpvWebsocketPlugin: () => c.hasPlugin,
|
||||
});
|
||||
assert.equal(deps.getTexthookerWebsocketUrl(), c.expected);
|
||||
assert.equal(context.getTexthookerWebsocketUrl(), c.expected);
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,171 @@
|
||||
import { createCliCommandContext } from './cli-command-context';
|
||||
import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps';
|
||||
import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps';
|
||||
import type { CliArgs, CliCommandSource } from '../../cli/args';
|
||||
import { resolveTexthookerWebsocketUrl } from '../../core/services/startup';
|
||||
import { createCliCommandContext, type CliCommandContextFactoryDeps } from './cli-command-context';
|
||||
|
||||
type CliCommandContextMainDeps = Parameters<typeof createBuildCliCommandContextMainDepsHandler>[0];
|
||||
type CliCommandContextMainState = {
|
||||
mpvSocketPath: string;
|
||||
mpvClient: ReturnType<CliCommandContextFactoryDeps['getMpvClient']>;
|
||||
texthookerPort: number;
|
||||
overlayRuntimeInitialized: boolean;
|
||||
};
|
||||
|
||||
export type CliCommandContextMainDeps = {
|
||||
appState: CliCommandContextMainState;
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
onMpvSocketPathChanged?: (nextSocketPath: string, previousSocketPath: string) => void;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getResolvedConfig: () => {
|
||||
texthooker?: { openBrowser?: boolean };
|
||||
websocket?: { enabled?: boolean | 'auto'; port?: number };
|
||||
annotationWebsocket?: { enabled?: boolean; port?: number };
|
||||
};
|
||||
defaultWebsocketPort: number;
|
||||
defaultAnnotationWebsocketPort: number;
|
||||
hasMpvWebsocketPlugin: () => boolean;
|
||||
openExternal: (url: string) => Promise<unknown>;
|
||||
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
showPlaybackFeedback?: (text: string) => void;
|
||||
|
||||
initializeOverlayRuntime: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
togglePrimarySubtitleBar: () => void;
|
||||
openFirstRunSetupWindow: (force?: boolean) => void;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
|
||||
copyCurrentSubtitle: () => void;
|
||||
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWordCache: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
dispatchSessionAction: CliCommandContextFactoryDeps['dispatchSessionAction'];
|
||||
|
||||
getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
|
||||
clearAnilistToken: () => void;
|
||||
openAnilistSetupWindow: () => void;
|
||||
openJellyfinSetupWindow: () => void;
|
||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||
processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
|
||||
getCharacterDictionarySelection?: CliCommandContextFactoryDeps['getCharacterDictionarySelection'];
|
||||
setCharacterDictionarySelection?: CliCommandContextFactoryDeps['setCharacterDictionarySelection'];
|
||||
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
runUpdateCommand: CliCommandContextFactoryDeps['runUpdateCommand'];
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: (
|
||||
args: CliArgs,
|
||||
source: CliCommandSource,
|
||||
) => Promise<void>;
|
||||
runYoutubePlaybackFlow: CliCommandContextFactoryDeps['runYoutubePlaybackFlow'];
|
||||
ensureBackgroundStatsServer?: CliCommandContextFactoryDeps['ensureBackgroundStatsServer'];
|
||||
|
||||
openYomitanSettings: () => void;
|
||||
openHachidoriSettings: () => void;
|
||||
openConfigSettingsWindow: () => void;
|
||||
openSyncUiWindow: () => void;
|
||||
openYoutubeBrowserWindow: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
printHelp: () => void;
|
||||
stopApp: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
schedule: (fn: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
||||
logInfo: (message: string) => void;
|
||||
logDebug: (message: string) => void;
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
};
|
||||
|
||||
// Adapts main-process state and services to the CLI command context. The context deps are
|
||||
// built once; each call creates a fresh context over them.
|
||||
export function createCliCommandContextFactory(deps: CliCommandContextMainDeps) {
|
||||
const buildCliCommandContextMainDepsHandler = createBuildCliCommandContextMainDepsHandler(deps);
|
||||
const cliCommandContextMainDeps = buildCliCommandContextMainDepsHandler();
|
||||
const buildCliCommandContextDepsHandler =
|
||||
createBuildCliCommandContextDepsHandler(cliCommandContextMainDeps);
|
||||
const contextDeps: CliCommandContextFactoryDeps = {
|
||||
setLogLevel: deps.setLogLevel,
|
||||
getSocketPath: () => deps.appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
const previousSocketPath = deps.appState.mpvSocketPath;
|
||||
deps.appState.mpvSocketPath = socketPath;
|
||||
if (socketPath !== previousSocketPath) {
|
||||
deps.onMpvSocketPathChanged?.(socketPath, previousSocketPath);
|
||||
}
|
||||
},
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
showOsd: (text: string) => deps.showMpvOsd(text),
|
||||
showPlaybackFeedback: deps.showPlaybackFeedback,
|
||||
texthookerService: deps.texthookerService,
|
||||
getTexthookerPort: () => deps.appState.texthookerPort,
|
||||
setTexthookerPort: (port: number) => {
|
||||
deps.appState.texthookerPort = port;
|
||||
},
|
||||
getTexthookerWebsocketUrl: () =>
|
||||
resolveTexthookerWebsocketUrl(
|
||||
deps.getResolvedConfig(),
|
||||
{
|
||||
defaultWebsocketPort: deps.defaultWebsocketPort,
|
||||
defaultAnnotationWebsocketPort: deps.defaultAnnotationWebsocketPort,
|
||||
},
|
||||
deps.hasMpvWebsocketPlugin(),
|
||||
),
|
||||
shouldOpenBrowser: () => deps.getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openExternal: (url: string) => deps.openExternal(url),
|
||||
logBrowserOpenError: (url: string, error: unknown) => deps.logBrowserOpenError(url, error),
|
||||
isOverlayInitialized: () => deps.appState.overlayRuntimeInitialized,
|
||||
initializeOverlay: () => deps.initializeOverlayRuntime(),
|
||||
toggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
|
||||
togglePrimarySubtitleBar: () => deps.togglePrimarySubtitleBar(),
|
||||
openFirstRunSetup: (force?: boolean) => deps.openFirstRunSetupWindow(force),
|
||||
setVisibleOverlay: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||
copyCurrentSubtitle: () => deps.copyCurrentSubtitle(),
|
||||
startPendingMultiCopy: (timeoutMs: number) => deps.startPendingMultiCopy(timeoutMs),
|
||||
mineSentenceCard: () => deps.mineSentenceCard(),
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||
deps.startPendingMineSentenceMultiple(timeoutMs),
|
||||
updateLastCardFromClipboard: () => deps.updateLastCardFromClipboard(),
|
||||
refreshKnownWordCache: () => deps.refreshKnownWordCache(),
|
||||
triggerFieldGrouping: () => deps.triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => deps.markLastCardAsAudioCard(),
|
||||
dispatchSessionAction: (request) => deps.dispatchSessionAction(request),
|
||||
getAnilistStatus: () => deps.getAnilistStatus(),
|
||||
clearAnilistToken: () => deps.clearAnilistToken(),
|
||||
openAnilistSetup: () => deps.openAnilistSetupWindow(),
|
||||
openJellyfinSetup: () => deps.openJellyfinSetupWindow(),
|
||||
getAnilistQueueStatus: () => deps.getAnilistQueueStatus(),
|
||||
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
|
||||
generateCharacterDictionary: (targetPath?: string) =>
|
||||
deps.generateCharacterDictionary(targetPath),
|
||||
getCharacterDictionarySelection: deps.getCharacterDictionarySelection,
|
||||
setCharacterDictionarySelection: deps.setCharacterDictionarySelection,
|
||||
runStatsCommand: (args: CliArgs, source) => deps.runStatsCommand(args, source),
|
||||
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
|
||||
runUpdateCommand: (args: CliArgs, source: CliCommandSource) =>
|
||||
deps.runUpdateCommand(args, source),
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: (args: CliArgs, source: CliCommandSource) =>
|
||||
deps.runEnsureLinuxRuntimePluginAssetsCommand(args, source),
|
||||
runYoutubePlaybackFlow: (request) => deps.runYoutubePlaybackFlow(request),
|
||||
ensureBackgroundStatsServer: deps.ensureBackgroundStatsServer,
|
||||
openYomitanSettings: () => deps.openYomitanSettings(),
|
||||
openHachidoriSettings: () => deps.openHachidoriSettings(),
|
||||
openConfigSettingsWindow: () => deps.openConfigSettingsWindow(),
|
||||
openSyncUiWindow: () => deps.openSyncUiWindow(),
|
||||
openYoutubeBrowserWindow: () => deps.openYoutubeBrowserWindow(),
|
||||
cycleSecondarySubMode: () => deps.cycleSecondarySubMode(),
|
||||
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||
printHelp: () => deps.printHelp(),
|
||||
stopApp: () => deps.stopApp(),
|
||||
hasMainWindow: () => deps.hasMainWindow(),
|
||||
getMultiCopyTimeoutMs: () => deps.getMultiCopyTimeoutMs(),
|
||||
schedule: (fn: () => void, delayMs: number) => deps.schedule(fn, delayMs),
|
||||
logInfo: (message: string) => deps.logInfo(message),
|
||||
logDebug: (message: string) => deps.logDebug(message),
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
logError: (message: string, err: unknown) => deps.logError(message, err),
|
||||
};
|
||||
|
||||
return () => createCliCommandContext(buildCliCommandContextDepsHandler());
|
||||
return () => createCliCommandContext(contextDeps);
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { CliArgs, CliCommandSource } from '../../cli/args';
|
||||
import { resolveTexthookerWebsocketUrl } from '../../core/services/startup';
|
||||
import type { CliCommandContextFactoryDeps } from './cli-command-context';
|
||||
|
||||
type CliCommandContextMainState = {
|
||||
mpvSocketPath: string;
|
||||
mpvClient: ReturnType<CliCommandContextFactoryDeps['getMpvClient']>;
|
||||
texthookerPort: number;
|
||||
overlayRuntimeInitialized: boolean;
|
||||
};
|
||||
|
||||
export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
appState: CliCommandContextMainState;
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
onMpvSocketPathChanged?: (nextSocketPath: string, previousSocketPath: string) => void;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getResolvedConfig: () => {
|
||||
texthooker?: { openBrowser?: boolean };
|
||||
websocket?: { enabled?: boolean | 'auto'; port?: number };
|
||||
annotationWebsocket?: { enabled?: boolean; port?: number };
|
||||
};
|
||||
defaultWebsocketPort: number;
|
||||
defaultAnnotationWebsocketPort: number;
|
||||
hasMpvWebsocketPlugin: () => boolean;
|
||||
openExternal: (url: string) => Promise<unknown>;
|
||||
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||
showMpvOsd: (text: string) => void;
|
||||
showPlaybackFeedback?: (text: string) => void;
|
||||
|
||||
initializeOverlayRuntime: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
togglePrimarySubtitleBar: () => void;
|
||||
openFirstRunSetupWindow: (force?: boolean) => void;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
|
||||
copyCurrentSubtitle: () => void;
|
||||
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWordCache: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
dispatchSessionAction: CliCommandContextFactoryDeps['dispatchSessionAction'];
|
||||
|
||||
getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
|
||||
clearAnilistToken: () => void;
|
||||
openAnilistSetupWindow: () => void;
|
||||
openJellyfinSetupWindow: () => void;
|
||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||
processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
|
||||
getCharacterDictionarySelection?: CliCommandContextFactoryDeps['getCharacterDictionarySelection'];
|
||||
setCharacterDictionarySelection?: CliCommandContextFactoryDeps['setCharacterDictionarySelection'];
|
||||
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
runUpdateCommand: CliCommandContextFactoryDeps['runUpdateCommand'];
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: (
|
||||
args: CliArgs,
|
||||
source: CliCommandSource,
|
||||
) => Promise<void>;
|
||||
runYoutubePlaybackFlow: CliCommandContextFactoryDeps['runYoutubePlaybackFlow'];
|
||||
ensureBackgroundStatsServer?: CliCommandContextFactoryDeps['ensureBackgroundStatsServer'];
|
||||
|
||||
openYomitanSettings: () => void;
|
||||
openHachidoriSettings: () => void;
|
||||
openConfigSettingsWindow: () => void;
|
||||
openSyncUiWindow: () => void;
|
||||
openYoutubeBrowserWindow: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
printHelp: () => void;
|
||||
stopApp: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
schedule: (fn: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
||||
logInfo: (message: string) => void;
|
||||
logDebug: (message: string) => void;
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
}) {
|
||||
return (): CliCommandContextFactoryDeps => ({
|
||||
setLogLevel: deps.setLogLevel,
|
||||
getSocketPath: () => deps.appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
const previousSocketPath = deps.appState.mpvSocketPath;
|
||||
deps.appState.mpvSocketPath = socketPath;
|
||||
if (socketPath !== previousSocketPath) {
|
||||
deps.onMpvSocketPathChanged?.(socketPath, previousSocketPath);
|
||||
}
|
||||
},
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
showOsd: (text: string) => deps.showMpvOsd(text),
|
||||
showPlaybackFeedback: deps.showPlaybackFeedback,
|
||||
texthookerService: deps.texthookerService,
|
||||
getTexthookerPort: () => deps.appState.texthookerPort,
|
||||
setTexthookerPort: (port: number) => {
|
||||
deps.appState.texthookerPort = port;
|
||||
},
|
||||
getTexthookerWebsocketUrl: () =>
|
||||
resolveTexthookerWebsocketUrl(
|
||||
deps.getResolvedConfig(),
|
||||
{
|
||||
defaultWebsocketPort: deps.defaultWebsocketPort,
|
||||
defaultAnnotationWebsocketPort: deps.defaultAnnotationWebsocketPort,
|
||||
},
|
||||
deps.hasMpvWebsocketPlugin(),
|
||||
),
|
||||
shouldOpenBrowser: () => deps.getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openExternal: (url: string) => deps.openExternal(url),
|
||||
logBrowserOpenError: (url: string, error: unknown) => deps.logBrowserOpenError(url, error),
|
||||
isOverlayInitialized: () => deps.appState.overlayRuntimeInitialized,
|
||||
initializeOverlay: () => deps.initializeOverlayRuntime(),
|
||||
toggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
|
||||
togglePrimarySubtitleBar: () => deps.togglePrimarySubtitleBar(),
|
||||
openFirstRunSetup: (force?: boolean) => deps.openFirstRunSetupWindow(force),
|
||||
setVisibleOverlay: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||
copyCurrentSubtitle: () => deps.copyCurrentSubtitle(),
|
||||
startPendingMultiCopy: (timeoutMs: number) => deps.startPendingMultiCopy(timeoutMs),
|
||||
mineSentenceCard: () => deps.mineSentenceCard(),
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||
deps.startPendingMineSentenceMultiple(timeoutMs),
|
||||
updateLastCardFromClipboard: () => deps.updateLastCardFromClipboard(),
|
||||
refreshKnownWordCache: () => deps.refreshKnownWordCache(),
|
||||
triggerFieldGrouping: () => deps.triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => deps.markLastCardAsAudioCard(),
|
||||
dispatchSessionAction: (request) => deps.dispatchSessionAction(request),
|
||||
getAnilistStatus: () => deps.getAnilistStatus(),
|
||||
clearAnilistToken: () => deps.clearAnilistToken(),
|
||||
openAnilistSetup: () => deps.openAnilistSetupWindow(),
|
||||
openJellyfinSetup: () => deps.openJellyfinSetupWindow(),
|
||||
getAnilistQueueStatus: () => deps.getAnilistQueueStatus(),
|
||||
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
|
||||
generateCharacterDictionary: (targetPath?: string) =>
|
||||
deps.generateCharacterDictionary(targetPath),
|
||||
getCharacterDictionarySelection: deps.getCharacterDictionarySelection,
|
||||
setCharacterDictionarySelection: deps.setCharacterDictionarySelection,
|
||||
runStatsCommand: (args: CliArgs, source) => deps.runStatsCommand(args, source),
|
||||
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
|
||||
runUpdateCommand: (args: CliArgs, source: CliCommandSource) =>
|
||||
deps.runUpdateCommand(args, source),
|
||||
runEnsureLinuxRuntimePluginAssetsCommand: (args: CliArgs, source: CliCommandSource) =>
|
||||
deps.runEnsureLinuxRuntimePluginAssetsCommand(args, source),
|
||||
runYoutubePlaybackFlow: (request) => deps.runYoutubePlaybackFlow(request),
|
||||
ensureBackgroundStatsServer: deps.ensureBackgroundStatsServer,
|
||||
openYomitanSettings: () => deps.openYomitanSettings(),
|
||||
openHachidoriSettings: () => deps.openHachidoriSettings(),
|
||||
openConfigSettingsWindow: () => deps.openConfigSettingsWindow(),
|
||||
openSyncUiWindow: () => deps.openSyncUiWindow(),
|
||||
openYoutubeBrowserWindow: () => deps.openYoutubeBrowserWindow(),
|
||||
cycleSecondarySubMode: () => deps.cycleSecondarySubMode(),
|
||||
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||
printHelp: () => deps.printHelp(),
|
||||
stopApp: () => deps.stopApp(),
|
||||
hasMainWindow: () => deps.hasMainWindow(),
|
||||
getMultiCopyTimeoutMs: () => deps.getMultiCopyTimeoutMs(),
|
||||
schedule: (fn: () => void, delayMs: number) => deps.schedule(fn, delayMs),
|
||||
logInfo: (message: string) => deps.logInfo(message),
|
||||
logDebug: (message: string) => deps.logDebug(message),
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
logError: (message: string, err: unknown) => deps.logError(message, err),
|
||||
});
|
||||
}
|
||||
@@ -3,11 +3,3 @@ type ComposerShape = Record<string, unknown>;
|
||||
export type ComposerInputs<T extends ComposerShape> = Readonly<Required<T>>;
|
||||
|
||||
export type ComposerOutputs<T extends ComposerShape> = Readonly<T>;
|
||||
|
||||
export type BuiltMainDeps<TFactory> = TFactory extends (
|
||||
...args: infer _TFactoryArgs
|
||||
) => infer TBuilder
|
||||
? TBuilder extends (...args: infer _TBuilderArgs) => infer TDeps
|
||||
? TDeps
|
||||
: never
|
||||
: never;
|
||||
@@ -1,15 +1,14 @@
|
||||
import {
|
||||
createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler,
|
||||
createBuildHandleJellyfinRemotePlayMainDepsHandler,
|
||||
createBuildHandleJellyfinRemotePlaystateMainDepsHandler,
|
||||
createBuildReportJellyfinRemoteProgressMainDepsHandler,
|
||||
createBuildReportJellyfinRemoteStoppedMainDepsHandler,
|
||||
createHandleJellyfinRemoteGeneralCommand,
|
||||
createHandleJellyfinRemotePlay,
|
||||
createHandleJellyfinRemotePlaystate,
|
||||
createJellyfinRemoteReportTracker,
|
||||
createReportJellyfinRemoteProgressHandler,
|
||||
createReportJellyfinRemoteStoppedHandler,
|
||||
type JellyfinRemoteGeneralCommandHandlerDeps,
|
||||
type JellyfinRemotePlayHandlerDeps,
|
||||
type JellyfinRemotePlaystateHandlerDeps,
|
||||
type JellyfinRemoteProgressReporterDeps,
|
||||
} from '../domains/jellyfin';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
@@ -18,44 +17,24 @@ type RemotePlaystatePayload = Parameters<ReturnType<typeof createHandleJellyfinR
|
||||
type RemoteGeneralPayload = Parameters<
|
||||
ReturnType<typeof createHandleJellyfinRemoteGeneralCommand>
|
||||
>[0];
|
||||
type JellyfinRemotePlayMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinRemotePlayMainDepsHandler
|
||||
>[0];
|
||||
type JellyfinRemotePlaystateMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinRemotePlaystateMainDepsHandler
|
||||
>[0];
|
||||
type JellyfinRemoteGeneralMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler
|
||||
>[0];
|
||||
type JellyfinRemoteProgressMainDeps = Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0];
|
||||
|
||||
export type JellyfinRemoteComposerOptions = ComposerInputs<{
|
||||
getConfiguredSession: JellyfinRemotePlayMainDeps['getConfiguredSession'];
|
||||
getClientInfo: JellyfinRemotePlayMainDeps['getClientInfo'];
|
||||
getJellyfinConfig: JellyfinRemotePlayMainDeps['getJellyfinConfig'];
|
||||
playJellyfinItem: JellyfinRemotePlayMainDeps['playJellyfinItem'];
|
||||
logWarn: JellyfinRemotePlayMainDeps['logWarn'];
|
||||
getMpvClient: JellyfinRemoteProgressMainDeps['getMpvClient'];
|
||||
sendMpvCommand: JellyfinRemotePlaystateMainDeps['sendMpvCommand'];
|
||||
jellyfinTicksToSeconds: Parameters<
|
||||
typeof createBuildHandleJellyfinRemotePlaystateMainDepsHandler
|
||||
>[0]['jellyfinTicksToSeconds'];
|
||||
getActivePlayback: JellyfinRemoteGeneralMainDeps['getActivePlayback'];
|
||||
clearActivePlayback: JellyfinRemoteProgressMainDeps['clearActivePlayback'];
|
||||
getNow: JellyfinRemoteProgressMainDeps['getNow'];
|
||||
getLastProgressAtMs: Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0]['getLastProgressAtMs'];
|
||||
setLastProgressAtMs: Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0]['setLastProgressAtMs'];
|
||||
getConfiguredSession: JellyfinRemotePlayHandlerDeps['getConfiguredSession'];
|
||||
getClientInfo: JellyfinRemotePlayHandlerDeps['getClientInfo'];
|
||||
getJellyfinConfig: JellyfinRemotePlayHandlerDeps['getJellyfinConfig'];
|
||||
playJellyfinItem: JellyfinRemotePlayHandlerDeps['playJellyfinItem'];
|
||||
logWarn: JellyfinRemotePlayHandlerDeps['logWarn'];
|
||||
getMpvClient: JellyfinRemoteProgressReporterDeps['getMpvClient'];
|
||||
sendMpvCommand: JellyfinRemotePlaystateHandlerDeps['sendMpvCommand'];
|
||||
jellyfinTicksToSeconds: JellyfinRemotePlaystateHandlerDeps['jellyfinTicksToSeconds'];
|
||||
getActivePlayback: JellyfinRemoteGeneralCommandHandlerDeps['getActivePlayback'];
|
||||
clearActivePlayback: JellyfinRemoteProgressReporterDeps['clearActivePlayback'];
|
||||
getNow: JellyfinRemoteProgressReporterDeps['getNow'];
|
||||
getLastProgressAtMs: JellyfinRemoteProgressReporterDeps['getLastProgressAtMs'];
|
||||
setLastProgressAtMs: JellyfinRemoteProgressReporterDeps['setLastProgressAtMs'];
|
||||
progressIntervalMs: number;
|
||||
ticksPerSecond: number;
|
||||
logDebug: Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0]['logDebug'];
|
||||
logDebug: JellyfinRemoteProgressReporterDeps['logDebug'];
|
||||
}>;
|
||||
|
||||
export type JellyfinRemoteComposerResult = ComposerOutputs<{
|
||||
@@ -69,75 +48,54 @@ export type JellyfinRemoteComposerResult = ComposerOutputs<{
|
||||
export function composeJellyfinRemoteHandlers(
|
||||
options: JellyfinRemoteComposerOptions,
|
||||
): JellyfinRemoteComposerResult {
|
||||
const buildReportJellyfinRemoteProgressMainDepsHandler =
|
||||
createBuildReportJellyfinRemoteProgressMainDepsHandler({
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
clearActivePlayback: options.clearActivePlayback,
|
||||
getMpvClient: options.getMpvClient,
|
||||
getNow: options.getNow,
|
||||
getLastProgressAtMs: options.getLastProgressAtMs,
|
||||
setLastProgressAtMs: options.setLastProgressAtMs,
|
||||
progressIntervalMs: options.progressIntervalMs,
|
||||
ticksPerSecond: options.ticksPerSecond,
|
||||
logDebug: options.logDebug,
|
||||
});
|
||||
const buildReportJellyfinRemoteStoppedMainDepsHandler =
|
||||
createBuildReportJellyfinRemoteStoppedMainDepsHandler({
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
clearActivePlayback: options.clearActivePlayback,
|
||||
getMpvClient: options.getMpvClient,
|
||||
getNow: options.getNow,
|
||||
ticksPerSecond: options.ticksPerSecond,
|
||||
logDebug: options.logDebug,
|
||||
logWarn: options.logWarn,
|
||||
});
|
||||
const reportTracker = createJellyfinRemoteReportTracker();
|
||||
const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler({
|
||||
...buildReportJellyfinRemoteProgressMainDepsHandler(),
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
clearActivePlayback: options.clearActivePlayback,
|
||||
getMpvClient: options.getMpvClient,
|
||||
getNow: options.getNow,
|
||||
getLastProgressAtMs: options.getLastProgressAtMs,
|
||||
setLastProgressAtMs: options.setLastProgressAtMs,
|
||||
progressIntervalMs: options.progressIntervalMs,
|
||||
ticksPerSecond: options.ticksPerSecond,
|
||||
logDebug: options.logDebug,
|
||||
reportTracker,
|
||||
});
|
||||
const reportJellyfinRemoteStopped = createReportJellyfinRemoteStoppedHandler({
|
||||
...buildReportJellyfinRemoteStoppedMainDepsHandler(),
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
clearActivePlayback: options.clearActivePlayback,
|
||||
getMpvClient: options.getMpvClient,
|
||||
getNow: options.getNow,
|
||||
ticksPerSecond: options.ticksPerSecond,
|
||||
logDebug: options.logDebug,
|
||||
logWarn: options.logWarn,
|
||||
reportTracker,
|
||||
});
|
||||
|
||||
const buildHandleJellyfinRemotePlayMainDepsHandler =
|
||||
createBuildHandleJellyfinRemotePlayMainDepsHandler({
|
||||
return {
|
||||
reportJellyfinRemoteProgress,
|
||||
reportJellyfinRemoteStopped,
|
||||
handleJellyfinRemotePlay: createHandleJellyfinRemotePlay({
|
||||
getConfiguredSession: options.getConfiguredSession,
|
||||
getClientInfo: options.getClientInfo,
|
||||
getJellyfinConfig: options.getJellyfinConfig,
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
playJellyfinItem: options.playJellyfinItem,
|
||||
logWarn: options.logWarn,
|
||||
});
|
||||
const buildHandleJellyfinRemotePlaystateMainDepsHandler =
|
||||
createBuildHandleJellyfinRemotePlaystateMainDepsHandler({
|
||||
}),
|
||||
handleJellyfinRemotePlaystate: createHandleJellyfinRemotePlaystate({
|
||||
getMpvClient: options.getMpvClient,
|
||||
sendMpvCommand: options.sendMpvCommand,
|
||||
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
||||
reportJellyfinRemoteStopped: () => reportJellyfinRemoteStopped(),
|
||||
jellyfinTicksToSeconds: options.jellyfinTicksToSeconds,
|
||||
});
|
||||
const buildHandleJellyfinRemoteGeneralCommandMainDepsHandler =
|
||||
createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler({
|
||||
}),
|
||||
handleJellyfinRemoteGeneralCommand: createHandleJellyfinRemoteGeneralCommand({
|
||||
getMpvClient: options.getMpvClient,
|
||||
sendMpvCommand: options.sendMpvCommand,
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
||||
logDebug: (message) => options.logDebug(message, undefined),
|
||||
});
|
||||
|
||||
return {
|
||||
reportJellyfinRemoteProgress,
|
||||
reportJellyfinRemoteStopped,
|
||||
handleJellyfinRemotePlay: createHandleJellyfinRemotePlay(
|
||||
buildHandleJellyfinRemotePlayMainDepsHandler(),
|
||||
),
|
||||
handleJellyfinRemotePlaystate: createHandleJellyfinRemotePlaystate(
|
||||
buildHandleJellyfinRemotePlaystateMainDepsHandler(),
|
||||
),
|
||||
handleJellyfinRemoteGeneralCommand: createHandleJellyfinRemoteGeneralCommand(
|
||||
buildHandleJellyfinRemoteGeneralCommandMainDepsHandler(),
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import {
|
||||
buildJellyfinSetupFormHtml,
|
||||
createEnsureMpvConnectedForJellyfinPlaybackHandler,
|
||||
createBuildGetJellyfinClientInfoMainDepsHandler,
|
||||
createBuildGetResolvedJellyfinConfigMainDepsHandler,
|
||||
createBuildOpenJellyfinSetupWindowMainDepsHandler,
|
||||
createGetJellyfinClientInfoHandler,
|
||||
createGetResolvedJellyfinConfigHandler,
|
||||
createHandleJellyfinAuthCommands,
|
||||
@@ -35,17 +32,11 @@ type HandlePlayCommandMainDeps = Parameters<typeof createHandleJellyfinPlayComma
|
||||
type HandleRemoteAnnounceMainDeps = Parameters<typeof createHandleJellyfinRemoteAnnounceCommand>[0];
|
||||
type StartRemoteSessionMainDeps = Parameters<typeof createStartJellyfinRemoteSessionHandler>[0];
|
||||
type RunJellyfinCommandMainDeps = Parameters<typeof createRunJellyfinCommandHandler>[0];
|
||||
type OpenJellyfinSetupWindowMainDeps = Parameters<
|
||||
typeof createBuildOpenJellyfinSetupWindowMainDepsHandler
|
||||
>[0];
|
||||
type OpenJellyfinSetupWindowMainDeps = Parameters<typeof createOpenJellyfinSetupWindowHandler>[0];
|
||||
|
||||
export type JellyfinRuntimeComposerOptions = ComposerInputs<{
|
||||
getResolvedJellyfinConfigMainDeps: Parameters<
|
||||
typeof createBuildGetResolvedJellyfinConfigMainDepsHandler
|
||||
>[0];
|
||||
getJellyfinClientInfoMainDeps: Parameters<
|
||||
typeof createBuildGetJellyfinClientInfoMainDepsHandler
|
||||
>[0];
|
||||
getResolvedJellyfinConfigMainDeps: Parameters<typeof createGetResolvedJellyfinConfigHandler>[0];
|
||||
getJellyfinClientInfoMainDeps: Parameters<typeof createGetJellyfinClientInfoHandler>[0];
|
||||
waitForMpvConnectedMainDeps: Parameters<typeof createWaitForMpvConnectedHandler>[0];
|
||||
launchMpvIdleForJellyfinPlaybackMainDeps: Parameters<
|
||||
typeof createLaunchMpvIdleForJellyfinPlaybackHandler
|
||||
@@ -141,12 +132,10 @@ export function composeJellyfinRuntimeHandlers(
|
||||
options: JellyfinRuntimeComposerOptions,
|
||||
): JellyfinRuntimeComposerResult {
|
||||
const getResolvedJellyfinConfig = createGetResolvedJellyfinConfigHandler(
|
||||
createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
||||
options.getResolvedJellyfinConfigMainDeps,
|
||||
)(),
|
||||
options.getResolvedJellyfinConfigMainDeps,
|
||||
);
|
||||
const getJellyfinClientInfo = createGetJellyfinClientInfoHandler(
|
||||
createBuildGetJellyfinClientInfoMainDepsHandler(options.getJellyfinClientInfoMainDeps)(),
|
||||
options.getJellyfinClientInfoMainDeps,
|
||||
);
|
||||
|
||||
const waitForMpvConnected = createWaitForMpvConnectedHandler(options.waitForMpvConnectedMainDeps);
|
||||
@@ -233,16 +222,14 @@ export function composeJellyfinRuntimeHandlers(
|
||||
getCurrentSession: () => options.startJellyfinRemoteSessionMainDeps.getCurrentSession(),
|
||||
startJellyfinRemoteSession: (startOptions) => startJellyfinRemoteSession(startOptions),
|
||||
});
|
||||
const openJellyfinSetupWindow = createOpenJellyfinSetupWindowHandler(
|
||||
createBuildOpenJellyfinSetupWindowMainDepsHandler({
|
||||
...options.openJellyfinSetupWindowMainDeps,
|
||||
maybeFocusExistingSetupWindow: maybeFocusExistingJellyfinSetupWindow,
|
||||
getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
getJellyfinClientInfo: () => getJellyfinClientInfo(),
|
||||
restartRemoteSession: () => restartJellyfinRemoteSessionAfterSetupLogin(),
|
||||
stopRemoteSession: () => stopJellyfinRemoteSession(),
|
||||
})(),
|
||||
);
|
||||
const openJellyfinSetupWindow = createOpenJellyfinSetupWindowHandler({
|
||||
...options.openJellyfinSetupWindowMainDeps,
|
||||
maybeFocusExistingSetupWindow: maybeFocusExistingJellyfinSetupWindow,
|
||||
getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
getJellyfinClientInfo: () => getJellyfinClientInfo(),
|
||||
restartRemoteSession: () => restartJellyfinRemoteSessionAfterSetupLogin(),
|
||||
stopRemoteSession: () => stopJellyfinRemoteSession(),
|
||||
});
|
||||
|
||||
return {
|
||||
getResolvedJellyfinConfig,
|
||||
|
||||
@@ -423,3 +423,63 @@ test('composeMpvRuntimeHandlers reuses completed background tokenization warmups
|
||||
assert.equal(jlptWarmupCalls, 1);
|
||||
assert.equal(frequencyWarmupCalls, 1);
|
||||
});
|
||||
|
||||
type TokenizerMainDeps = Parameters<
|
||||
typeof composeMpvRuntimeHandlers
|
||||
>[0]['tokenizer']['buildTokenizerDepsMainDeps'];
|
||||
type TokenizerRuntimeOptions = Parameters<
|
||||
Parameters<typeof composeMpvRuntimeHandlers>[0]['tokenizer']['createTokenizerRuntimeDeps']
|
||||
>[0];
|
||||
|
||||
async function captureTokenizerRuntimeOptions(
|
||||
overrides: Partial<TokenizerMainDeps>,
|
||||
): Promise<TokenizerRuntimeOptions> {
|
||||
const fixture = createDefaultMpvFixture();
|
||||
let captured: TokenizerRuntimeOptions | null = null;
|
||||
const composed = composeMpvRuntimeHandlers<
|
||||
{ connect: () => void; on: () => void },
|
||||
TokenizerRuntimeOptions,
|
||||
{ text: string }
|
||||
>({
|
||||
...fixture,
|
||||
tokenizer: {
|
||||
...fixture.tokenizer,
|
||||
buildTokenizerDepsMainDeps: { ...fixture.tokenizer.buildTokenizerDepsMainDeps, ...overrides },
|
||||
createTokenizerRuntimeDeps: (deps) => {
|
||||
captured = deps;
|
||||
return deps;
|
||||
},
|
||||
tokenizeSubtitle: async (text) => ({ text }),
|
||||
},
|
||||
});
|
||||
await composed.tokenizeSubtitle('line');
|
||||
assert.ok(captured);
|
||||
return captured;
|
||||
}
|
||||
|
||||
test('composeMpvRuntimeHandlers records every known-word lookup result', async () => {
|
||||
const lookups: boolean[] = [];
|
||||
const runtimeOptions = await captureTokenizerRuntimeOptions({
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: (hit) => lookups.push(hit),
|
||||
});
|
||||
|
||||
assert.equal(runtimeOptions.isKnownWord('known'), true);
|
||||
assert.equal(runtimeOptions.isKnownWord('unknown'), false);
|
||||
assert.deepEqual(lookups, [true, false]);
|
||||
});
|
||||
|
||||
test('composeMpvRuntimeHandlers disables name matching while the character dictionary is disabled', async () => {
|
||||
let characterDictionaryEnabled = false;
|
||||
const runtimeOptions = await captureTokenizerRuntimeOptions({
|
||||
getCharacterDictionaryEnabled: () => characterDictionaryEnabled,
|
||||
getNameMatchEnabled: () => true,
|
||||
getNameMatchImagesEnabled: () => true,
|
||||
});
|
||||
|
||||
assert.equal(runtimeOptions.getNameMatchEnabled?.(), false);
|
||||
assert.equal(runtimeOptions.getNameMatchImagesEnabled?.(), false);
|
||||
characterDictionaryEnabled = true;
|
||||
assert.equal(runtimeOptions.getNameMatchEnabled?.(), true);
|
||||
assert.equal(runtimeOptions.getNameMatchImagesEnabled?.(), true);
|
||||
});
|
||||
@@ -1,47 +1,54 @@
|
||||
import { createBindMpvMainEventHandlersHandler } from '../mpv-main-event-bindings';
|
||||
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main-event-main-deps';
|
||||
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps';
|
||||
import { createMpvMainEventBindingDeps } from '../mpv-main-event-binding-deps';
|
||||
import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service';
|
||||
import type { MpvClientRuntimeServiceOptions } from '../mpv-client-runtime-service';
|
||||
import type { Config } from '../../../types';
|
||||
import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics';
|
||||
import {
|
||||
createBuildTokenizerDepsMainHandler,
|
||||
createCreateMecabTokenizerAndCheckMainHandler,
|
||||
createPrewarmSubtitleDictionariesMainHandler,
|
||||
} from '../subtitle-tokenization-main-deps';
|
||||
createMecabTokenizerAndCheckHandler,
|
||||
createPrewarmSubtitleDictionariesHandler,
|
||||
} from '../subtitle-tokenization-warmup';
|
||||
import type { TokenizerDepsRuntimeOptions } from '../../../core/services/tokenizer';
|
||||
import {
|
||||
createLaunchBackgroundWarmupTaskHandler as createLaunchBackgroundWarmupTaskFromStartup,
|
||||
createStartBackgroundWarmupsHandler as createStartBackgroundWarmupsFromStartup,
|
||||
} from '../startup-warmups';
|
||||
import type { BuiltMainDeps, ComposerInputs, ComposerOutputs } from './contracts';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type BindMpvMainEventHandlersMainDeps = Parameters<
|
||||
typeof createBuildBindMpvMainEventHandlersMainDepsHandler
|
||||
>[0];
|
||||
type BindMpvMainEventHandlersMainDeps = Parameters<typeof createMpvMainEventBindingDeps>[0];
|
||||
type BindMpvMainEventHandlers = ReturnType<typeof createBindMpvMainEventHandlersHandler>;
|
||||
type BoundMpvClient = Parameters<BindMpvMainEventHandlers>[0];
|
||||
type RuntimeMpvClient = BoundMpvClient & { connect: () => void };
|
||||
type MpvClientRuntimeServiceFactoryMainDeps<TMpvClient extends RuntimeMpvClient> = Omit<
|
||||
Parameters<
|
||||
typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>
|
||||
>[0],
|
||||
Parameters<typeof createMpvClientRuntimeServiceFactory<TMpvClient>>[0],
|
||||
'bindEventHandlers'
|
||||
>;
|
||||
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<
|
||||
typeof createUpdateMpvSubtitleRenderMetricsHandler
|
||||
>[0];
|
||||
type BuildTokenizerDepsMainDeps = Parameters<typeof createBuildTokenizerDepsMainHandler>[0];
|
||||
type TokenizerMainDeps = BuiltMainDeps<typeof createBuildTokenizerDepsMainHandler>;
|
||||
// Tokenizer deps as main.ts supplies them: the core runtime options plus lookup recording and
|
||||
// the character-dictionary gate applied in buildTokenizerRuntimeOptions.
|
||||
type BuildTokenizerDepsMainDeps = TokenizerDepsRuntimeOptions & {
|
||||
getJlptEnabled: NonNullable<TokenizerDepsRuntimeOptions['getJlptEnabled']>;
|
||||
getCharacterDictionaryEnabled?: () => boolean;
|
||||
getFrequencyDictionaryEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
|
||||
>;
|
||||
getFrequencyDictionaryMatchMode: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryMatchMode']
|
||||
>;
|
||||
getFrequencyRank: NonNullable<TokenizerDepsRuntimeOptions['getFrequencyRank']>;
|
||||
getMinSentenceWordsForNPlusOne: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getMinSentenceWordsForNPlusOne']
|
||||
>;
|
||||
getYomitanGroupDebugEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getYomitanGroupDebugEnabled']
|
||||
>;
|
||||
recordLookup: (hit: boolean) => void;
|
||||
};
|
||||
type CreateMecabTokenizerAndCheckMainDeps = Parameters<
|
||||
typeof createCreateMecabTokenizerAndCheckMainHandler
|
||||
typeof createMecabTokenizerAndCheckHandler
|
||||
>[0];
|
||||
type PrewarmSubtitleDictionariesMainDeps = Parameters<
|
||||
typeof createPrewarmSubtitleDictionariesMainHandler
|
||||
typeof createPrewarmSubtitleDictionariesHandler
|
||||
>[0];
|
||||
type LaunchBackgroundWarmupTaskMainDeps = Parameters<
|
||||
typeof createLaunchBackgroundWarmupTaskFromStartup
|
||||
@@ -61,7 +68,7 @@ export type MpvRuntimeComposerOptions<
|
||||
updateMpvSubtitleRenderMetricsMainDeps: UpdateMpvSubtitleRenderMetricsMainDeps;
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: BuildTokenizerDepsMainDeps;
|
||||
createTokenizerRuntimeDeps: (deps: TokenizerMainDeps) => TTokenizerRuntimeDeps;
|
||||
createTokenizerRuntimeDeps: (deps: TokenizerDepsRuntimeOptions) => TTokenizerRuntimeDeps;
|
||||
tokenizeSubtitle: (text: string, deps: TTokenizerRuntimeDeps) => Promise<TTokenizedSubtitle>;
|
||||
createMecabTokenizerAndCheckMainDeps: CreateMecabTokenizerAndCheckMainDeps;
|
||||
prewarmSubtitleDictionariesMainDeps: PrewarmSubtitleDictionariesMainDeps;
|
||||
@@ -88,6 +95,39 @@ export type MpvRuntimeComposerResult<
|
||||
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
||||
}>;
|
||||
|
||||
// Adapts main's tokenizer deps to the core runtime options: every known-word lookup is
|
||||
// recorded, and name matching is off while the character dictionary is disabled. Returns a
|
||||
// fresh object, since the composer wraps onTokenizationReady on it.
|
||||
function buildTokenizerRuntimeOptions(
|
||||
deps: BuildTokenizerDepsMainDeps,
|
||||
): TokenizerDepsRuntimeOptions {
|
||||
const {
|
||||
recordLookup,
|
||||
getCharacterDictionaryEnabled,
|
||||
getNameMatchEnabled,
|
||||
getNameMatchImagesEnabled,
|
||||
...runtimeOptions
|
||||
} = deps;
|
||||
const characterDictionaryEnabled = (): boolean => getCharacterDictionaryEnabled?.() !== false;
|
||||
return {
|
||||
...runtimeOptions,
|
||||
isKnownWord: (text, reading, options) => {
|
||||
const hit = deps.isKnownWord(text, reading, options);
|
||||
recordLookup(hit);
|
||||
return hit;
|
||||
},
|
||||
...(getNameMatchEnabled
|
||||
? { getNameMatchEnabled: () => characterDictionaryEnabled() && getNameMatchEnabled() }
|
||||
: {}),
|
||||
...(getNameMatchImagesEnabled
|
||||
? {
|
||||
getNameMatchImagesEnabled: () =>
|
||||
characterDictionaryEnabled() && getNameMatchImagesEnabled(),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function composeMpvRuntimeHandlers<
|
||||
TMpvClient extends RuntimeMpvClient,
|
||||
TTokenizerRuntimeDeps,
|
||||
@@ -95,36 +135,23 @@ export function composeMpvRuntimeHandlers<
|
||||
>(
|
||||
options: MpvRuntimeComposerOptions<TMpvClient, TTokenizerRuntimeDeps, TTokenizedSubtitle>,
|
||||
): MpvRuntimeComposerResult<TMpvClient, TTokenizedSubtitle> {
|
||||
const bindMpvMainEventHandlersMainDeps = createBuildBindMpvMainEventHandlersMainDepsHandler(
|
||||
options.bindMpvMainEventHandlersMainDeps,
|
||||
)();
|
||||
const bindMpvClientEventHandlers = createBindMpvMainEventHandlersHandler(
|
||||
bindMpvMainEventHandlersMainDeps,
|
||||
createMpvMainEventBindingDeps(options.bindMpvMainEventHandlersMainDeps),
|
||||
);
|
||||
|
||||
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
||||
createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>({
|
||||
...options.mpvClientRuntimeServiceFactoryMainDeps,
|
||||
bindEventHandlers: (client) => bindMpvClientEventHandlers(client),
|
||||
});
|
||||
const createMpvClientRuntimeService = (): TMpvClient =>
|
||||
createMpvClientRuntimeServiceFactory(buildMpvClientRuntimeServiceFactoryMainDepsHandler())();
|
||||
const createMpvClientRuntimeService = createMpvClientRuntimeServiceFactory<TMpvClient>({
|
||||
...options.mpvClientRuntimeServiceFactoryMainDeps,
|
||||
bindEventHandlers: (client) => bindMpvClientEventHandlers(client),
|
||||
});
|
||||
|
||||
const updateMpvSubtitleRenderMetrics = createUpdateMpvSubtitleRenderMetricsHandler(
|
||||
options.updateMpvSubtitleRenderMetricsMainDeps,
|
||||
);
|
||||
|
||||
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler(
|
||||
options.tokenizer.buildTokenizerDepsMainDeps,
|
||||
);
|
||||
const createMecabTokenizerAndCheck = createCreateMecabTokenizerAndCheckMainHandler(
|
||||
const createMecabTokenizerAndCheck = createMecabTokenizerAndCheckHandler(
|
||||
options.tokenizer.createMecabTokenizerAndCheckMainDeps,
|
||||
);
|
||||
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
|
||||
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesHandler(
|
||||
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
||||
);
|
||||
const shouldInitializeMecabForAnnotations = (): boolean => {
|
||||
@@ -227,7 +254,9 @@ export function composeMpvRuntimeHandlers<
|
||||
if (cachedTokenizerRuntimeDeps) {
|
||||
return cachedTokenizerRuntimeDeps;
|
||||
}
|
||||
const tokenizerMainDeps = buildTokenizerDepsHandler();
|
||||
const tokenizerMainDeps = buildTokenizerRuntimeOptions(
|
||||
options.tokenizer.buildTokenizerDepsMainDeps,
|
||||
);
|
||||
const baseOnTokenizationReady = tokenizerMainDeps.onTokenizationReady;
|
||||
tokenizerMainDeps.onTokenizationReady = (tokenizedText: string): void => {
|
||||
if (!shouldWarmupAnnotationDictionaries()) {
|
||||
|
||||
@@ -3,22 +3,16 @@ import {
|
||||
createRestoreWindowsOnActivateHandler,
|
||||
createShouldRestoreWindowsOnActivateHandler,
|
||||
} from '../app-lifecycle-actions';
|
||||
import { createBuildOnWillQuitCleanupDepsHandler } from '../app-lifecycle-main-cleanup';
|
||||
import {
|
||||
createBuildRestoreWindowsOnActivateMainDepsHandler,
|
||||
createBuildShouldRestoreWindowsOnActivateMainDepsHandler,
|
||||
} from '../app-lifecycle-main-activate';
|
||||
import { createOnWillQuitCleanupDeps } from '../app-lifecycle-main-cleanup';
|
||||
import { registerProtocolUrlHandlers } from '../protocol-url-handlers';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type RegisterProtocolUrlHandlersMainDeps = Parameters<typeof registerProtocolUrlHandlers>[0];
|
||||
type OnWillQuitCleanupDeps = Parameters<typeof createBuildOnWillQuitCleanupDepsHandler>[0];
|
||||
type OnWillQuitCleanupDeps = Parameters<typeof createOnWillQuitCleanupDeps>[0];
|
||||
type ShouldRestoreWindowsOnActivateMainDeps = Parameters<
|
||||
typeof createBuildShouldRestoreWindowsOnActivateMainDepsHandler
|
||||
>[0];
|
||||
type RestoreWindowsOnActivateMainDeps = Parameters<
|
||||
typeof createBuildRestoreWindowsOnActivateMainDepsHandler
|
||||
typeof createShouldRestoreWindowsOnActivateHandler
|
||||
>[0];
|
||||
type RestoreWindowsOnActivateMainDeps = Parameters<typeof createRestoreWindowsOnActivateHandler>[0];
|
||||
|
||||
export type StartupLifecycleComposerOptions = ComposerInputs<{
|
||||
registerProtocolUrlHandlersMainDeps: RegisterProtocolUrlHandlersMainDeps;
|
||||
@@ -38,15 +32,13 @@ export function composeStartupLifecycleHandlers(
|
||||
options: StartupLifecycleComposerOptions,
|
||||
): StartupLifecycleComposerResult {
|
||||
const onWillQuitCleanupHandler = createOnWillQuitCleanupHandler(
|
||||
createBuildOnWillQuitCleanupDepsHandler(options.onWillQuitCleanupMainDeps)(),
|
||||
createOnWillQuitCleanupDeps(options.onWillQuitCleanupMainDeps),
|
||||
);
|
||||
const shouldRestoreWindowsOnActivateHandler = createShouldRestoreWindowsOnActivateHandler(
|
||||
createBuildShouldRestoreWindowsOnActivateMainDepsHandler(
|
||||
options.shouldRestoreWindowsOnActivateMainDeps,
|
||||
)(),
|
||||
options.shouldRestoreWindowsOnActivateMainDeps,
|
||||
);
|
||||
const restoreWindowsOnActivateHandler = createRestoreWindowsOnActivateHandler(
|
||||
createBuildRestoreWindowsOnActivateMainDepsHandler(options.restoreWindowsOnActivateMainDeps)(),
|
||||
options.restoreWindowsOnActivateMainDeps,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildConfigHotReloadPayload,
|
||||
createConfigHotReloadAppliedHandler,
|
||||
createConfigHotReloadMessageHandler,
|
||||
createWatchConfigPathHandler,
|
||||
} from './config-hot-reload-handlers';
|
||||
|
||||
test('createConfigHotReloadAppliedHandler runs all hot-reload effects', () => {
|
||||
@@ -328,3 +329,38 @@ test('createConfigHotReloadMessageHandler routes message through configured noti
|
||||
'notify:SubMiner:Config reload failed',
|
||||
]);
|
||||
});
|
||||
|
||||
test('watch config path handler watches file directly when config exists', () => {
|
||||
const calls: string[] = [];
|
||||
const watchConfigPath = createWatchConfigPathHandler({
|
||||
fileExists: () => true,
|
||||
dirname: (path) => path.split('/').slice(0, -1).join('/'),
|
||||
watchPath: (targetPath, nextListener) => {
|
||||
calls.push(`watch:${targetPath}`);
|
||||
nextListener('change', 'ignored');
|
||||
return { close: () => calls.push('close') };
|
||||
},
|
||||
});
|
||||
|
||||
const watcher = watchConfigPath('/tmp/config.jsonc', () => calls.push('change'));
|
||||
watcher.close();
|
||||
assert.deepEqual(calls, ['watch:/tmp/config.jsonc', 'change', 'close']);
|
||||
});
|
||||
|
||||
test('watch config path handler filters directory events to config files only', () => {
|
||||
const calls: string[] = [];
|
||||
const watchConfigPath = createWatchConfigPathHandler({
|
||||
fileExists: () => false,
|
||||
dirname: (path) => path.split('/').slice(0, -1).join('/'),
|
||||
watchPath: (_targetPath, nextListener) => {
|
||||
nextListener('change', 'foo.txt');
|
||||
nextListener('change', 'config.json');
|
||||
nextListener('change', 'config.jsonc');
|
||||
nextListener('change', null);
|
||||
return { close: () => {} };
|
||||
},
|
||||
});
|
||||
|
||||
watchConfigPath('/tmp/config.jsonc', () => calls.push('change'));
|
||||
assert.deepEqual(calls, ['change', 'change', 'change']);
|
||||
});
|
||||
@@ -228,6 +228,35 @@ export function createConfigHotReloadMessageHandler(deps: ConfigHotReloadMessage
|
||||
};
|
||||
}
|
||||
|
||||
type ConfigWatchListener = (eventType: string, filename: string | null) => void;
|
||||
|
||||
// Watches the config file, or its directory (filtered to config.json[c]) when the file
|
||||
// does not exist yet, and calls onChange for relevant events.
|
||||
export function createWatchConfigPathHandler(deps: {
|
||||
fileExists: (path: string) => boolean;
|
||||
dirname: (path: string) => string;
|
||||
watchPath: (targetPath: string, listener: ConfigWatchListener) => { close: () => void };
|
||||
}) {
|
||||
return (configPath: string, onChange: () => void): { close: () => void } => {
|
||||
const watchTarget = deps.fileExists(configPath) ? configPath : deps.dirname(configPath);
|
||||
const watcher = deps.watchPath(watchTarget, (_eventType, filename) => {
|
||||
if (watchTarget === configPath) {
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized =
|
||||
typeof filename === 'string' ? filename : filename ? String(filename) : undefined;
|
||||
if (!normalized || normalized === 'config.json' || normalized === 'config.jsonc') {
|
||||
onChange();
|
||||
}
|
||||
});
|
||||
return {
|
||||
close: () => watcher.close(),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRestartRequiredConfigMessage(fields: string[]): string {
|
||||
return `Config updated; restart required for: ${fields.join(', ')}`;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createWatchConfigPathHandler } from './config-hot-reload-main-deps';
|
||||
|
||||
test('watch config path handler watches file directly when config exists', () => {
|
||||
const calls: string[] = [];
|
||||
const watchConfigPath = createWatchConfigPathHandler({
|
||||
fileExists: () => true,
|
||||
dirname: (path) => path.split('/').slice(0, -1).join('/'),
|
||||
watchPath: (targetPath, nextListener) => {
|
||||
calls.push(`watch:${targetPath}`);
|
||||
nextListener('change', 'ignored');
|
||||
return { close: () => calls.push('close') };
|
||||
},
|
||||
});
|
||||
|
||||
const watcher = watchConfigPath('/tmp/config.jsonc', () => calls.push('change'));
|
||||
watcher.close();
|
||||
assert.deepEqual(calls, ['watch:/tmp/config.jsonc', 'change', 'close']);
|
||||
});
|
||||
|
||||
test('watch config path handler filters directory events to config files only', () => {
|
||||
const calls: string[] = [];
|
||||
const watchConfigPath = createWatchConfigPathHandler({
|
||||
fileExists: () => false,
|
||||
dirname: (path) => path.split('/').slice(0, -1).join('/'),
|
||||
watchPath: (_targetPath, nextListener) => {
|
||||
nextListener('change', 'foo.txt');
|
||||
nextListener('change', 'config.json');
|
||||
nextListener('change', 'config.jsonc');
|
||||
nextListener('change', null);
|
||||
return { close: () => {} };
|
||||
},
|
||||
});
|
||||
|
||||
watchConfigPath('/tmp/config.jsonc', () => calls.push('change'));
|
||||
assert.deepEqual(calls, ['change', 'change', 'change']);
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import type {
|
||||
ConfigHotReloadDiff,
|
||||
ConfigHotReloadRuntimeDeps,
|
||||
} from '../../core/services/config-hot-reload';
|
||||
import type { ReloadConfigStrictResult } from '../../config';
|
||||
import type { AnkiConnectConfig } from '../../types/anki';
|
||||
import type {
|
||||
ConfigHotReloadPayload,
|
||||
ConfigValidationWarning,
|
||||
ResolvedConfig,
|
||||
SecondarySubMode,
|
||||
} from '../../types';
|
||||
import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers';
|
||||
|
||||
type ConfigWatchListener = (eventType: string, filename: string | null) => void;
|
||||
|
||||
export function createWatchConfigPathHandler(deps: {
|
||||
fileExists: (path: string) => boolean;
|
||||
dirname: (path: string) => string;
|
||||
watchPath: (targetPath: string, listener: ConfigWatchListener) => { close: () => void };
|
||||
}) {
|
||||
return (configPath: string, onChange: () => void): { close: () => void } => {
|
||||
const watchTarget = deps.fileExists(configPath) ? configPath : deps.dirname(configPath);
|
||||
const watcher = deps.watchPath(watchTarget, (_eventType, filename) => {
|
||||
if (watchTarget === configPath) {
|
||||
onChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized =
|
||||
typeof filename === 'string' ? filename : filename ? String(filename) : undefined;
|
||||
if (!normalized || normalized === 'config.json' || normalized === 'config.jsonc') {
|
||||
onChange();
|
||||
}
|
||||
});
|
||||
return {
|
||||
close: () => watcher.close(),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type WatchConfigPathMainDeps = Parameters<typeof createWatchConfigPathHandler>[0];
|
||||
type ConfigHotReloadMessageMainDeps = Parameters<typeof createConfigHotReloadMessageHandler>[0];
|
||||
|
||||
export function createBuildWatchConfigPathMainDepsHandler(deps: WatchConfigPathMainDeps) {
|
||||
return (): WatchConfigPathMainDeps => ({
|
||||
fileExists: (targetPath: string) => deps.fileExists(targetPath),
|
||||
dirname: (targetPath: string) => deps.dirname(targetPath),
|
||||
watchPath: (targetPath: string, listener: ConfigWatchListener) =>
|
||||
deps.watchPath(targetPath, listener),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildConfigHotReloadMessageMainDepsHandler(
|
||||
deps: ConfigHotReloadMessageMainDeps,
|
||||
) {
|
||||
return (): ConfigHotReloadMessageMainDeps => ({
|
||||
getNotificationType: () => deps.getNotificationType?.(),
|
||||
showMpvOsd: (message: string) => deps.showMpvOsd(message),
|
||||
showOverlayNotification: (payload) => deps.showOverlayNotification?.(payload),
|
||||
showDesktopNotification: (title: string, options: { body: string }) =>
|
||||
deps.showDesktopNotification(title, options),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildConfigHotReloadAppliedMainDepsHandler(deps: {
|
||||
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) => void;
|
||||
setSessionBindings: (
|
||||
sessionBindings: ConfigHotReloadPayload['sessionBindings'],
|
||||
sessionBindingWarnings: ConfigHotReloadPayload['sessionBindingWarnings'],
|
||||
) => void;
|
||||
refreshGlobalAndOverlayShortcuts: () => void;
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
||||
applyAnkiRuntimeConfigPatch: (patch: Partial<AnkiConnectConfig>) => void;
|
||||
invalidateTokenizationCache?: () => void;
|
||||
refreshSubtitlePrefetch?: () => void;
|
||||
refreshCurrentSubtitle?: () => void;
|
||||
setLogLevel?: (level: ResolvedConfig['logging']['level']) => void;
|
||||
setLogRotation?: (rotation: ResolvedConfig['logging']['rotation']) => void;
|
||||
setLogFileToggles?: (files: ResolvedConfig['logging']['files']) => void;
|
||||
applyAniSkipConfig?: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) =>
|
||||
deps.setKeybindings(keybindings),
|
||||
setSessionBindings: (
|
||||
sessionBindings: ConfigHotReloadPayload['sessionBindings'],
|
||||
sessionBindingWarnings: ConfigHotReloadPayload['sessionBindingWarnings'],
|
||||
) => deps.setSessionBindings(sessionBindings, sessionBindingWarnings),
|
||||
refreshGlobalAndOverlayShortcuts: () => deps.refreshGlobalAndOverlayShortcuts(),
|
||||
setSecondarySubMode: (mode: SecondarySubMode) => deps.setSecondarySubMode(mode),
|
||||
broadcastToOverlayWindows: (channel: string, payload: unknown) =>
|
||||
deps.broadcastToOverlayWindows(channel, payload),
|
||||
applyAnkiRuntimeConfigPatch: (patch: Partial<AnkiConnectConfig>) =>
|
||||
deps.applyAnkiRuntimeConfigPatch(patch),
|
||||
invalidateTokenizationCache: () => deps.invalidateTokenizationCache?.(),
|
||||
refreshSubtitlePrefetch: () => deps.refreshSubtitlePrefetch?.(),
|
||||
refreshCurrentSubtitle: () => deps.refreshCurrentSubtitle?.(),
|
||||
setLogLevel: (level: ResolvedConfig['logging']['level']) => deps.setLogLevel?.(level),
|
||||
setLogRotation: (rotation: ResolvedConfig['logging']['rotation']) =>
|
||||
deps.setLogRotation?.(rotation),
|
||||
setLogFileToggles: (files: ResolvedConfig['logging']['files']) =>
|
||||
deps.setLogFileToggles?.(files),
|
||||
applyAniSkipConfig: () => deps.applyAniSkipConfig?.(),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildConfigHotReloadRuntimeMainDepsHandler(deps: {
|
||||
getCurrentConfig: () => ResolvedConfig;
|
||||
reloadConfigStrict: () => ReloadConfigStrictResult;
|
||||
watchConfigPath: ConfigHotReloadRuntimeDeps['watchConfigPath'];
|
||||
setTimeout: (callback: () => void, delayMs: number) => NodeJS.Timeout;
|
||||
clearTimeout: (timeout: NodeJS.Timeout) => void;
|
||||
debounceMs: number;
|
||||
onHotReloadApplied: (diff: ConfigHotReloadDiff, config: ResolvedConfig) => void;
|
||||
onRestartRequired: (fields: string[]) => void;
|
||||
onInvalidConfig: (message: string) => void;
|
||||
onValidationWarnings: (configPath: string, warnings: ConfigValidationWarning[]) => void;
|
||||
}) {
|
||||
return (): ConfigHotReloadRuntimeDeps => ({
|
||||
getCurrentConfig: () => deps.getCurrentConfig(),
|
||||
reloadConfigStrict: () => deps.reloadConfigStrict(),
|
||||
watchConfigPath: (configPath, onChange) => deps.watchConfigPath(configPath, onChange),
|
||||
setTimeout: (callback: () => void, delayMs: number) => deps.setTimeout(callback, delayMs),
|
||||
clearTimeout: (timeout: NodeJS.Timeout) => deps.clearTimeout(timeout),
|
||||
debounceMs: deps.debounceMs,
|
||||
onHotReloadApplied: (diff, config) => deps.onHotReloadApplied(diff, config),
|
||||
onRestartRequired: (fields: string[]) => deps.onRestartRequired(fields),
|
||||
onInvalidConfig: (message: string) => deps.onInvalidConfig(message),
|
||||
onValidationWarnings: (configPath: string, warnings: ConfigValidationWarning[]) =>
|
||||
deps.onValidationWarnings(configPath, warnings),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildDictionaryRootsHandler } from './dictionary-roots';
|
||||
|
||||
const darwinDeps = {
|
||||
platform: 'darwin',
|
||||
dirname: '/repo/dist/main',
|
||||
appPath: '/Applications/SubMiner.app/Contents/Resources/app.asar',
|
||||
resourcesPath: '/Applications/SubMiner.app/Contents/Resources',
|
||||
userDataPath: '/Users/a/.config/SubMiner',
|
||||
appUserDataPath: '/Users/a/Library/Application Support/SubMiner',
|
||||
homeDir: '/Users/a',
|
||||
cwd: '/repo',
|
||||
joinPath: (...parts: string[]) => parts.join('/'),
|
||||
} as const;
|
||||
|
||||
const win32Deps = {
|
||||
platform: 'win32',
|
||||
dirname: 'C:\\repo\\dist\\main',
|
||||
appPath: 'C:\\Program Files\\SubMiner\\resources\\app.asar',
|
||||
resourcesPath: 'C:\\Program Files\\SubMiner\\resources',
|
||||
userDataPath: 'C:\\Users\\a\\AppData\\Roaming\\SubMiner',
|
||||
appUserDataPath: 'C:\\Users\\a\\AppData\\Roaming\\SubMiner',
|
||||
homeDir: 'C:\\Users\\a',
|
||||
appDataDir: 'C:\\Users\\a\\AppData\\Roaming',
|
||||
cwd: 'C:\\repo',
|
||||
joinPath: (...parts: string[]) => parts.join('\\'),
|
||||
} as const;
|
||||
|
||||
for (const vendorDirName of ['yomitan-jlpt-vocab', 'frequency-dictionary']) {
|
||||
test(`dictionary roots for ${vendorDirName} list vendor, user data, platform and cwd roots`, () => {
|
||||
const roots = createBuildDictionaryRootsHandler({ ...darwinDeps, vendorDirName })();
|
||||
|
||||
assert.equal(roots.length, 11);
|
||||
assert.equal(roots[0], `/repo/dist/main/../../vendor/${vendorDirName}`);
|
||||
assert.equal(roots[2], `/Applications/SubMiner.app/Contents/Resources/${vendorDirName}`);
|
||||
assert.equal(roots[10], '/repo');
|
||||
});
|
||||
}
|
||||
|
||||
test('dictionary roots use APPDATA-style roots on windows', () => {
|
||||
const roots = createBuildDictionaryRootsHandler({
|
||||
...win32Deps,
|
||||
vendorDirName: 'yomitan-jlpt-vocab',
|
||||
})();
|
||||
|
||||
assert.equal(roots.includes('C:\\Users\\a\\.config\\SubMiner'), false);
|
||||
assert.equal(roots.includes('C:\\Users\\a\\AppData\\Roaming\\SubMiner'), true);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// Candidate root directories searched for a bundled dictionary (JLPT vocab, frequency, ...).
|
||||
// `vendorDirName` is the dictionary's folder under `vendor/` in dev and in packaged resources.
|
||||
export function createBuildDictionaryRootsHandler(deps: {
|
||||
vendorDirName: string;
|
||||
platform: NodeJS.Platform;
|
||||
dirname: string;
|
||||
appPath: string;
|
||||
resourcesPath: string;
|
||||
userDataPath: string;
|
||||
appUserDataPath: string;
|
||||
homeDir: string;
|
||||
appDataDir?: string;
|
||||
cwd: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
}) {
|
||||
return (): string[] => {
|
||||
const platformRoots =
|
||||
deps.platform === 'win32'
|
||||
? [
|
||||
deps.joinPath(
|
||||
deps.appDataDir ?? deps.joinPath(deps.homeDir, 'AppData', 'Roaming'),
|
||||
'SubMiner',
|
||||
),
|
||||
deps.joinPath(
|
||||
deps.appDataDir ?? deps.joinPath(deps.homeDir, 'AppData', 'Roaming'),
|
||||
'subminer',
|
||||
),
|
||||
]
|
||||
: [
|
||||
deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, '.config', 'subminer'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
|
||||
];
|
||||
|
||||
return [
|
||||
deps.joinPath(deps.dirname, '..', '..', 'vendor', deps.vendorDirName),
|
||||
deps.joinPath(deps.appPath, 'vendor', deps.vendorDirName),
|
||||
deps.joinPath(deps.resourcesPath, deps.vendorDirName),
|
||||
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', deps.vendorDirName),
|
||||
deps.userDataPath,
|
||||
deps.appUserDataPath,
|
||||
...platformRoots,
|
||||
deps.cwd,
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createBuildDictionaryRootsMainHandler,
|
||||
createBuildFrequencyDictionaryRootsMainHandler,
|
||||
} from './dictionary-runtime-main-deps';
|
||||
|
||||
test('dictionary roots main handler returns expected root list', () => {
|
||||
const roots = createBuildDictionaryRootsMainHandler({
|
||||
platform: 'darwin',
|
||||
dirname: '/repo/dist/main',
|
||||
appPath: '/Applications/SubMiner.app/Contents/Resources/app.asar',
|
||||
resourcesPath: '/Applications/SubMiner.app/Contents/Resources',
|
||||
userDataPath: '/Users/a/.config/SubMiner',
|
||||
appUserDataPath: '/Users/a/Library/Application Support/SubMiner',
|
||||
homeDir: '/Users/a',
|
||||
cwd: '/repo',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
})();
|
||||
|
||||
assert.equal(roots.length, 11);
|
||||
assert.equal(roots[0], '/repo/dist/main/../../vendor/yomitan-jlpt-vocab');
|
||||
assert.equal(roots[10], '/repo');
|
||||
});
|
||||
|
||||
test('frequency dictionary roots main handler returns expected root list', () => {
|
||||
const roots = createBuildFrequencyDictionaryRootsMainHandler({
|
||||
platform: 'darwin',
|
||||
dirname: '/repo/dist/main',
|
||||
appPath: '/Applications/SubMiner.app/Contents/Resources/app.asar',
|
||||
resourcesPath: '/Applications/SubMiner.app/Contents/Resources',
|
||||
userDataPath: '/Users/a/.config/SubMiner',
|
||||
appUserDataPath: '/Users/a/Library/Application Support/SubMiner',
|
||||
homeDir: '/Users/a',
|
||||
cwd: '/repo',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
})();
|
||||
|
||||
assert.equal(roots.length, 11);
|
||||
assert.equal(roots[0], '/repo/dist/main/../../vendor/frequency-dictionary');
|
||||
assert.equal(roots[10], '/repo');
|
||||
});
|
||||
|
||||
test('dictionary roots main handler uses APPDATA-style roots on windows', () => {
|
||||
const roots = createBuildDictionaryRootsMainHandler({
|
||||
platform: 'win32',
|
||||
dirname: 'C:\\repo\\dist\\main',
|
||||
appPath: 'C:\\Program Files\\SubMiner\\resources\\app.asar',
|
||||
resourcesPath: 'C:\\Program Files\\SubMiner\\resources',
|
||||
userDataPath: 'C:\\Users\\a\\AppData\\Roaming\\SubMiner',
|
||||
appUserDataPath: 'C:\\Users\\a\\AppData\\Roaming\\SubMiner',
|
||||
homeDir: 'C:\\Users\\a',
|
||||
appDataDir: 'C:\\Users\\a\\AppData\\Roaming',
|
||||
cwd: 'C:\\repo',
|
||||
joinPath: (...parts) => parts.join('\\'),
|
||||
})();
|
||||
|
||||
assert.equal(roots.includes('C:\\Users\\a\\.config\\SubMiner'), false);
|
||||
assert.equal(roots.includes('C:\\Users\\a\\AppData\\Roaming\\SubMiner'), true);
|
||||
});
|
||||
|
||||
test('frequency dictionary roots main handler uses APPDATA-style roots on windows', () => {
|
||||
const roots = createBuildFrequencyDictionaryRootsMainHandler({
|
||||
platform: 'win32',
|
||||
dirname: 'C:\\repo\\dist\\main',
|
||||
appPath: 'C:\\Program Files\\SubMiner\\resources\\app.asar',
|
||||
resourcesPath: 'C:\\Program Files\\SubMiner\\resources',
|
||||
userDataPath: 'C:\\Users\\a\\AppData\\Roaming\\SubMiner',
|
||||
appUserDataPath: 'C:\\Users\\a\\AppData\\Roaming\\SubMiner',
|
||||
homeDir: 'C:\\Users\\a',
|
||||
appDataDir: 'C:\\Users\\a\\AppData\\Roaming',
|
||||
cwd: 'C:\\repo',
|
||||
joinPath: (...parts) => parts.join('\\'),
|
||||
})();
|
||||
|
||||
assert.equal(roots.includes('C:\\Users\\a\\.config\\SubMiner'), false);
|
||||
assert.equal(roots.includes('C:\\Users\\a\\AppData\\Roaming\\SubMiner'), true);
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
import type { FrequencyDictionaryLookup, JlptLevel } from '../../types';
|
||||
|
||||
type JlptLookup = (term: string) => JlptLevel | null;
|
||||
|
||||
export function createBuildDictionaryRootsMainHandler(deps: {
|
||||
platform: NodeJS.Platform;
|
||||
dirname: string;
|
||||
appPath: string;
|
||||
resourcesPath: string;
|
||||
userDataPath: string;
|
||||
appUserDataPath: string;
|
||||
homeDir: string;
|
||||
appDataDir?: string;
|
||||
cwd: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
}) {
|
||||
return () => {
|
||||
const platformRoots =
|
||||
deps.platform === 'win32'
|
||||
? [
|
||||
deps.joinPath(
|
||||
deps.appDataDir ?? deps.joinPath(deps.homeDir, 'AppData', 'Roaming'),
|
||||
'SubMiner',
|
||||
),
|
||||
deps.joinPath(
|
||||
deps.appDataDir ?? deps.joinPath(deps.homeDir, 'AppData', 'Roaming'),
|
||||
'subminer',
|
||||
),
|
||||
]
|
||||
: [
|
||||
deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, '.config', 'subminer'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
|
||||
];
|
||||
|
||||
return [
|
||||
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'),
|
||||
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'),
|
||||
deps.userDataPath,
|
||||
deps.appUserDataPath,
|
||||
...platformRoots,
|
||||
deps.cwd,
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
export function createBuildFrequencyDictionaryRootsMainHandler(deps: {
|
||||
platform: NodeJS.Platform;
|
||||
dirname: string;
|
||||
appPath: string;
|
||||
resourcesPath: string;
|
||||
userDataPath: string;
|
||||
appUserDataPath: string;
|
||||
homeDir: string;
|
||||
appDataDir?: string;
|
||||
cwd: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
}) {
|
||||
return () => {
|
||||
const platformRoots =
|
||||
deps.platform === 'win32'
|
||||
? [
|
||||
deps.joinPath(
|
||||
deps.appDataDir ?? deps.joinPath(deps.homeDir, 'AppData', 'Roaming'),
|
||||
'SubMiner',
|
||||
),
|
||||
deps.joinPath(
|
||||
deps.appDataDir ?? deps.joinPath(deps.homeDir, 'AppData', 'Roaming'),
|
||||
'subminer',
|
||||
),
|
||||
]
|
||||
: [
|
||||
deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, '.config', 'subminer'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
|
||||
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
|
||||
];
|
||||
|
||||
return [
|
||||
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'frequency-dictionary'),
|
||||
deps.joinPath(deps.appPath, 'vendor', 'frequency-dictionary'),
|
||||
deps.joinPath(deps.resourcesPath, 'frequency-dictionary'),
|
||||
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'frequency-dictionary'),
|
||||
deps.userDataPath,
|
||||
deps.appUserDataPath,
|
||||
...platformRoots,
|
||||
deps.cwd,
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
|
||||
isJlptEnabled: () => boolean;
|
||||
getDictionaryRoots: () => string[];
|
||||
getJlptDictionarySearchPaths: (deps: { getDictionaryRoots: () => string[] }) => string[];
|
||||
setJlptLevelLookup: (lookup: JlptLookup) => void;
|
||||
logInfo: (message: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isJlptEnabled: () => deps.isJlptEnabled(),
|
||||
getSearchPaths: () =>
|
||||
deps.getJlptDictionarySearchPaths({
|
||||
getDictionaryRoots: () => deps.getDictionaryRoots(),
|
||||
}),
|
||||
setJlptLevelLookup: (lookup: JlptLookup) => deps.setJlptLevelLookup(lookup),
|
||||
log: (message: string) => deps.logInfo(`[JLPT] ${message}`),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildFrequencyDictionaryRuntimeMainDepsHandler(deps: {
|
||||
isFrequencyDictionaryEnabled: () => boolean;
|
||||
getDictionaryRoots: () => string[];
|
||||
getFrequencyDictionarySearchPaths: (deps: {
|
||||
getDictionaryRoots: () => string[];
|
||||
getSourcePath: () => string | undefined;
|
||||
}) => string[];
|
||||
getSourcePath: () => string | undefined;
|
||||
setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) => void;
|
||||
logInfo: (message: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isFrequencyDictionaryEnabled: () => deps.isFrequencyDictionaryEnabled(),
|
||||
getSearchPaths: () =>
|
||||
deps.getFrequencyDictionarySearchPaths({
|
||||
getDictionaryRoots: () =>
|
||||
deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot),
|
||||
getSourcePath: () => deps.getSourcePath(),
|
||||
}),
|
||||
setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) =>
|
||||
deps.setFrequencyRankLookup(lookup),
|
||||
log: (message: string) => deps.logInfo(`[Frequency] ${message}`),
|
||||
});
|
||||
}
|
||||
@@ -3,16 +3,13 @@ export * from '../jellyfin-cli-list';
|
||||
export * from '../jellyfin-cli-play';
|
||||
export * from '../jellyfin-cli-remote-announce';
|
||||
export * from '../jellyfin-client-info';
|
||||
export * from '../jellyfin-client-info-main-deps';
|
||||
export * from '../jellyfin-command-dispatch';
|
||||
export * from '../jellyfin-device-identity';
|
||||
export * from '../jellyfin-playback-launch';
|
||||
export * from '../jellyfin-remote-commands';
|
||||
export * from '../jellyfin-remote-connection';
|
||||
export * from '../jellyfin-remote-main-deps';
|
||||
export * from '../jellyfin-remote-playback';
|
||||
export * from '../jellyfin-remote-session-lifecycle';
|
||||
export * from '../jellyfin-setup-window';
|
||||
export * from '../jellyfin-setup-window-main-deps';
|
||||
export * from '../jellyfin-subtitle-preload';
|
||||
export * from '../mpv-jellyfin-defaults';
|
||||
@@ -1,8 +1,7 @@
|
||||
export * from '../mpv-client-runtime-service';
|
||||
export * from '../mpv-client-runtime-service-main-deps';
|
||||
export * from '../mpv-main-event-bindings';
|
||||
export * from '../mpv-main-event-main-deps';
|
||||
export * from '../mpv-main-event-binding-deps';
|
||||
export * from '../mpv-osd-runtime-handlers';
|
||||
export * from '../mpv-subtitle-render-metrics';
|
||||
export * from '../secondary-sub-mode-runtime-handler';
|
||||
export * from '../subtitle-tokenization-main-deps';
|
||||
export * from '../subtitle-tokenization-warmup';
|
||||
@@ -1,13 +1,10 @@
|
||||
export * from '../config-hot-reload-handlers';
|
||||
export * from '../config-hot-reload-main-deps';
|
||||
export * from '../field-grouping-resolver';
|
||||
export * from '../overlay-main-actions';
|
||||
export * from '../overlay-runtime-bootstrap-handlers';
|
||||
export * from '../overlay-runtime-main-actions';
|
||||
export * from '../overlay-visibility-runtime';
|
||||
export * from '../overlay-visibility-runtime-main-deps';
|
||||
export * from '../overlay-window-layout';
|
||||
export * from '../overlay-window-layout-main-deps';
|
||||
export * from '../overlay-window-runtime-handlers';
|
||||
export * from '../subtitle-position';
|
||||
export * from '../tray-runtime';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from '../global-shortcuts-runtime-handlers';
|
||||
export * from '../numeric-shortcut-session-runtime-handlers';
|
||||
export * from '../overlay-shortcuts-runtime-handlers';
|
||||
export * from '../overlay-shortcuts-runtime-main-deps';
|
||||
@@ -1,9 +1,8 @@
|
||||
export * from '../app-lifecycle-actions';
|
||||
export * from '../app-lifecycle-main-activate';
|
||||
export * from '../app-lifecycle-main-cleanup';
|
||||
export * from '../clipboard-queue';
|
||||
export * from '../config-derived';
|
||||
export * from '../dictionary-runtime-main-deps';
|
||||
export * from '../dictionary-roots';
|
||||
export * from '../immersion-media';
|
||||
export * from '../immersion-startup';
|
||||
export * from '../startup-config';
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type {
|
||||
createGetJellyfinClientInfoHandler,
|
||||
createGetResolvedJellyfinConfigHandler,
|
||||
} from './jellyfin-client-info';
|
||||
|
||||
type GetResolvedJellyfinConfigMainDeps = Parameters<
|
||||
typeof createGetResolvedJellyfinConfigHandler
|
||||
>[0];
|
||||
type GetJellyfinClientInfoMainDeps = Parameters<typeof createGetJellyfinClientInfoHandler>[0];
|
||||
|
||||
export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
||||
deps: GetResolvedJellyfinConfigMainDeps,
|
||||
) {
|
||||
return (): GetResolvedJellyfinConfigMainDeps => ({
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
loadStoredSession: () => deps.loadStoredSession(),
|
||||
getEnv: (name: string) => deps.getEnv(name),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildGetJellyfinClientInfoMainDepsHandler(
|
||||
deps: GetJellyfinClientInfoMainDeps,
|
||||
) {
|
||||
return (): GetJellyfinClientInfoMainDeps => ({
|
||||
getResolvedJellyfinConfig: () => deps.getResolvedJellyfinConfig(),
|
||||
getHostName: deps.getHostName ? () => deps.getHostName?.() || '' : undefined,
|
||||
defaultClientName: deps.defaultClientName,
|
||||
defaultClientVersion: deps.defaultClientVersion,
|
||||
});
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import type {
|
||||
JellyfinRemoteGeneralCommandHandlerDeps,
|
||||
JellyfinRemotePlayHandlerDeps,
|
||||
JellyfinRemotePlaystateHandlerDeps,
|
||||
} from './jellyfin-remote-commands';
|
||||
import type {
|
||||
JellyfinRemoteProgressReporterDeps,
|
||||
JellyfinRemoteStoppedReporterDeps,
|
||||
} from './jellyfin-remote-playback';
|
||||
|
||||
export function createBuildHandleJellyfinRemotePlayMainDepsHandler(
|
||||
deps: JellyfinRemotePlayHandlerDeps,
|
||||
) {
|
||||
return (): JellyfinRemotePlayHandlerDeps => ({
|
||||
getConfiguredSession: () => deps.getConfiguredSession(),
|
||||
getClientInfo: () => deps.getClientInfo(),
|
||||
getJellyfinConfig: () => deps.getJellyfinConfig(),
|
||||
...(deps.getActivePlayback
|
||||
? { getActivePlayback: () => deps.getActivePlayback?.() ?? null }
|
||||
: {}),
|
||||
playJellyfinItem: (params) => deps.playJellyfinItem(params),
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildHandleJellyfinRemotePlaystateMainDepsHandler(
|
||||
deps: JellyfinRemotePlaystateHandlerDeps,
|
||||
) {
|
||||
return (): JellyfinRemotePlaystateHandlerDeps => ({
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
sendMpvCommand: (client, command) => deps.sendMpvCommand(client, command),
|
||||
reportJellyfinRemoteProgress: (force: boolean) => deps.reportJellyfinRemoteProgress(force),
|
||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||
jellyfinTicksToSeconds: (ticks: number) => deps.jellyfinTicksToSeconds(ticks),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler(
|
||||
deps: JellyfinRemoteGeneralCommandHandlerDeps,
|
||||
) {
|
||||
return (): JellyfinRemoteGeneralCommandHandlerDeps => ({
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
sendMpvCommand: (client, command) => deps.sendMpvCommand(client, command),
|
||||
getActivePlayback: () => deps.getActivePlayback(),
|
||||
reportJellyfinRemoteProgress: (force: boolean) => deps.reportJellyfinRemoteProgress(force),
|
||||
logDebug: (message: string) => deps.logDebug(message),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildReportJellyfinRemoteProgressMainDepsHandler(
|
||||
deps: JellyfinRemoteProgressReporterDeps,
|
||||
) {
|
||||
return (): JellyfinRemoteProgressReporterDeps => ({
|
||||
getActivePlayback: () => deps.getActivePlayback(),
|
||||
clearActivePlayback: () => deps.clearActivePlayback(),
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
getNow: () => deps.getNow(),
|
||||
getLastProgressAtMs: () => deps.getLastProgressAtMs(),
|
||||
setLastProgressAtMs: (value: number) => deps.setLastProgressAtMs(value),
|
||||
progressIntervalMs: deps.progressIntervalMs,
|
||||
ticksPerSecond: deps.ticksPerSecond,
|
||||
logDebug: (message: string, error: unknown) => deps.logDebug(message, error),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildReportJellyfinRemoteStoppedMainDepsHandler(
|
||||
deps: JellyfinRemoteStoppedReporterDeps,
|
||||
) {
|
||||
return (): JellyfinRemoteStoppedReporterDeps => ({
|
||||
getActivePlayback: () => deps.getActivePlayback(),
|
||||
clearActivePlayback: () => deps.clearActivePlayback(),
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
getNow: deps.getNow ? () => deps.getNow?.() ?? Date.now() : undefined,
|
||||
ticksPerSecond: deps.ticksPerSecond,
|
||||
logDebug: (message: string, error: unknown) => deps.logDebug(message, error),
|
||||
...(deps.logWarn ? { logWarn: (message: string) => deps.logWarn?.(message) } : {}),
|
||||
});
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { createOpenJellyfinSetupWindowHandler } from './jellyfin-setup-window';
|
||||
|
||||
type OpenJellyfinSetupWindowMainDeps = Parameters<typeof createOpenJellyfinSetupWindowHandler>[0];
|
||||
|
||||
export function createBuildOpenJellyfinSetupWindowMainDepsHandler(
|
||||
deps: OpenJellyfinSetupWindowMainDeps,
|
||||
) {
|
||||
return (): OpenJellyfinSetupWindowMainDeps => ({
|
||||
maybeFocusExistingSetupWindow: () => deps.maybeFocusExistingSetupWindow(),
|
||||
createSetupWindow: () => deps.createSetupWindow(),
|
||||
getResolvedJellyfinConfig: () => deps.getResolvedJellyfinConfig(),
|
||||
buildSetupFormHtml: (state) => deps.buildSetupFormHtml(state),
|
||||
parseSubmissionUrl: (rawUrl: string) => deps.parseSubmissionUrl(rawUrl),
|
||||
authenticateWithPassword: (server: string, username: string, password: string, clientInfo) =>
|
||||
deps.authenticateWithPassword(server, username, password, clientInfo),
|
||||
getJellyfinClientInfo: () => deps.getJellyfinClientInfo(),
|
||||
saveStoredSession: (session) => deps.saveStoredSession(session),
|
||||
clearStoredSession: () => deps.clearStoredSession(),
|
||||
patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session),
|
||||
persistAuthenticatedSession: deps.persistAuthenticatedSession
|
||||
? (session, clientInfo) => deps.persistAuthenticatedSession?.(session, clientInfo)
|
||||
: undefined,
|
||||
restartRemoteSession: deps.restartRemoteSession
|
||||
? () => deps.restartRemoteSession?.()
|
||||
: undefined,
|
||||
stopRemoteSession: deps.stopRemoteSession ? () => deps.stopRemoteSession?.() : undefined,
|
||||
logInfo: (message: string) => deps.logInfo(message),
|
||||
logError: (message: string, error: unknown) => deps.logError(message, error),
|
||||
showMpvOsd: (message: string) => deps.showMpvOsd(message),
|
||||
clearSetupWindow: () => deps.clearSetupWindow(),
|
||||
setSetupWindow: (window) => deps.setSetupWindow(window),
|
||||
registerSetupIpcHandler: deps.registerSetupIpcHandler
|
||||
? (handler) => deps.registerSetupIpcHandler?.(handler) ?? (() => undefined)
|
||||
: undefined,
|
||||
encodeURIComponent: (value: string) => deps.encodeURIComponent(value),
|
||||
defaultServerUrl: deps.defaultServerUrl,
|
||||
hasStoredSession: () => deps.hasStoredSession(),
|
||||
});
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TClient,
|
||||
TResolvedConfig,
|
||||
TOptions,
|
||||
>(deps: {
|
||||
createClient: new (socketPath: string, options: TOptions) => TClient;
|
||||
getSocketPath: () => string;
|
||||
getResolvedConfig: () => TResolvedConfig;
|
||||
isAutoStartOverlayEnabled: () => boolean;
|
||||
setOverlayVisible: (visible: boolean) => void;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
|
||||
shouldAutoLoadSecondarySubTrack?: (path: string) => boolean;
|
||||
shouldQuitOnMpvShutdown?: () => boolean;
|
||||
requestAppQuit?: () => void;
|
||||
bindEventHandlers: (client: TClient) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createClient: deps.createClient,
|
||||
socketPath: deps.getSocketPath(),
|
||||
options: {
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
autoStartOverlay: deps.isAutoStartOverlayEnabled(),
|
||||
setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible),
|
||||
isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(),
|
||||
getReconnectTimer: () => deps.getReconnectTimer(),
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) =>
|
||||
deps.setReconnectTimer(timer),
|
||||
shouldAutoLoadSecondarySubTrack: deps.shouldAutoLoadSecondarySubTrack
|
||||
? (path: string) => deps.shouldAutoLoadSecondarySubTrack?.(path) ?? true
|
||||
: undefined,
|
||||
shouldQuitOnMpvShutdown: () => deps.shouldQuitOnMpvShutdown?.() ?? false,
|
||||
requestAppQuit: () => deps.requestAppQuit?.(),
|
||||
},
|
||||
bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { DEFAULT_CONFIG } from '../../config';
|
||||
import {
|
||||
createMpvClientRuntimeServiceFactory,
|
||||
type MpvClientRuntimeServiceOptions,
|
||||
} from './mpv-client-runtime-service';
|
||||
|
||||
test('mpv client factory reads socket path and auto-start per client, binds before connect', () => {
|
||||
const calls: string[] = [];
|
||||
let socketPath = '/tmp/a.sock';
|
||||
let autoStart = false;
|
||||
|
||||
class FakeClient {
|
||||
constructor(
|
||||
readonly socketPath: string,
|
||||
readonly options: MpvClientRuntimeServiceOptions,
|
||||
) {}
|
||||
connect() {
|
||||
calls.push(`connect:${this.socketPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const createClient = createMpvClientRuntimeServiceFactory({
|
||||
createClient: FakeClient,
|
||||
getSocketPath: () => socketPath,
|
||||
getResolvedConfig: () => DEFAULT_CONFIG,
|
||||
isAutoStartOverlayEnabled: () => autoStart,
|
||||
setOverlayVisible: () => {},
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
bindEventHandlers: (client) => calls.push(`bind:${client.socketPath}`),
|
||||
});
|
||||
|
||||
const first = createClient();
|
||||
socketPath = '/tmp/b.sock';
|
||||
autoStart = true;
|
||||
const second = createClient();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'bind:/tmp/a.sock',
|
||||
'connect:/tmp/a.sock',
|
||||
'bind:/tmp/b.sock',
|
||||
'connect:/tmp/b.sock',
|
||||
]);
|
||||
assert.equal(first.options.autoStartOverlay, false);
|
||||
assert.equal(second.options.autoStartOverlay, true);
|
||||
assert.equal(first.options.shouldQuitOnMpvShutdown?.(), false);
|
||||
});
|
||||
@@ -16,22 +16,35 @@ type MpvClientLike = {
|
||||
connect: () => void;
|
||||
};
|
||||
|
||||
type MpvClientCtor<
|
||||
TClient extends MpvClientLike,
|
||||
TOptions extends MpvClientRuntimeServiceOptions,
|
||||
> = new (socketPath: string, options: TOptions) => TClient;
|
||||
|
||||
export function createMpvClientRuntimeServiceFactory<
|
||||
TClient extends MpvClientLike,
|
||||
TOptions extends MpvClientRuntimeServiceOptions,
|
||||
>(deps: {
|
||||
createClient: MpvClientCtor<TClient, TOptions>;
|
||||
socketPath: string;
|
||||
options: TOptions;
|
||||
// Creates, binds and connects a new mpv client. Socket path and auto-start state are read
|
||||
// on each call, so a reconnect picks up the current values.
|
||||
export function createMpvClientRuntimeServiceFactory<TClient extends MpvClientLike>(deps: {
|
||||
createClient: new (socketPath: string, options: MpvClientRuntimeServiceOptions) => TClient;
|
||||
getSocketPath: () => string;
|
||||
getResolvedConfig: () => Config;
|
||||
isAutoStartOverlayEnabled: () => boolean;
|
||||
setOverlayVisible: (visible: boolean) => void;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
|
||||
shouldAutoLoadSecondarySubTrack?: (path: string) => boolean;
|
||||
shouldQuitOnMpvShutdown?: () => boolean;
|
||||
requestAppQuit?: () => void;
|
||||
bindEventHandlers: (client: TClient) => void;
|
||||
}) {
|
||||
return (): TClient => {
|
||||
const mpvClient = new deps.createClient(deps.socketPath, deps.options);
|
||||
const socketPath = deps.getSocketPath();
|
||||
const mpvClient = new deps.createClient(socketPath, {
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
autoStartOverlay: deps.isAutoStartOverlayEnabled(),
|
||||
setOverlayVisible: (visible) => deps.setOverlayVisible(visible),
|
||||
isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(),
|
||||
getReconnectTimer: () => deps.getReconnectTimer(),
|
||||
setReconnectTimer: (timer) => deps.setReconnectTimer(timer),
|
||||
shouldAutoLoadSecondarySubTrack: deps.shouldAutoLoadSecondarySubTrack,
|
||||
shouldQuitOnMpvShutdown: () => deps.shouldQuitOnMpvShutdown?.() ?? false,
|
||||
requestAppQuit: () => deps.requestAppQuit?.(),
|
||||
});
|
||||
deps.bindEventHandlers(mpvClient);
|
||||
mpvClient.connect();
|
||||
return mpvClient;
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseAssCues } from '../../core/services/subtitle-cue-parser';
|
||||
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './mpv-main-event-main-deps';
|
||||
import { createMpvMainEventBindingDeps } from './mpv-main-event-binding-deps';
|
||||
|
||||
type MainDeps = Parameters<typeof createBuildBindMpvMainEventHandlersMainDepsHandler>[0];
|
||||
type MainDeps = Parameters<typeof createMpvMainEventBindingDeps>[0];
|
||||
|
||||
/**
|
||||
* Builds the handlers with inert deps. `appStateOverrides` is merged over a blank app
|
||||
@@ -24,7 +24,7 @@ function buildMainDeps(
|
||||
previousSecondarySubVisibility: false,
|
||||
...appStateOverrides,
|
||||
};
|
||||
const handlers = createBuildBindMpvMainEventHandlersMainDepsHandler({
|
||||
const handlers = createMpvMainEventBindingDeps({
|
||||
appState,
|
||||
getQuitOnDisconnectArmed: () => false,
|
||||
scheduleQuitCheck: () => {},
|
||||
@@ -49,7 +49,7 @@ function buildMainDeps(
|
||||
updateSubtitleRenderMetrics: () => {},
|
||||
refreshDiscordPresence: () => {},
|
||||
...depsOverrides,
|
||||
})();
|
||||
});
|
||||
return { handlers, appState };
|
||||
}
|
||||
|
||||
+5
-3
@@ -11,7 +11,9 @@ type AnilistPostWatchRunOptions = {
|
||||
watchedSeconds?: number;
|
||||
};
|
||||
|
||||
export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
// Adapts main-process state to the mpv main-event bindings and owns the subtitle-recording
|
||||
// state (dedup gates, recorded cue keys, session epoch) that those bindings share.
|
||||
export function createMpvMainEventBindingDeps(deps: {
|
||||
appState: {
|
||||
initialArgs?: {
|
||||
jellyfinPlay?: unknown;
|
||||
@@ -149,7 +151,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
deps.appState.initialArgs?.youtubePlay,
|
||||
);
|
||||
|
||||
return () => ({
|
||||
return {
|
||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||
onMpvConnected: deps.onMpvConnected ? () => deps.onMpvConnected!() : undefined,
|
||||
@@ -407,5 +409,5 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
setPreviousSecondarySubVisibility: (visible: boolean) => {
|
||||
deps.appState.previousSecondarySubVisibility = visible;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -20,11 +20,6 @@ import {
|
||||
createUpdateVisibleOverlayBoundsHandler,
|
||||
hasLiveOverlayWindowBoundsMismatch,
|
||||
} from './overlay-window-layout';
|
||||
import {
|
||||
createBuildEnforceOverlayLayerOrderMainDepsHandler,
|
||||
createBuildEnsureOverlayWindowLevelMainDepsHandler,
|
||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler,
|
||||
} from './overlay-window-layout-main-deps';
|
||||
import { shouldSuppressVisibleOverlayRaiseForSeparateWindow } from './settings-window-z-order';
|
||||
|
||||
const LINUX_VISIBLE_OVERLAY_FULLSCREEN_GEOMETRY_GRACE_MS = 1_200;
|
||||
@@ -147,68 +142,57 @@ export function createOverlayGeometryRuntime(deps: OverlayGeometryRuntimeDeps) {
|
||||
});
|
||||
}
|
||||
|
||||
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
||||
getCurrentOverlayWindowBounds: () => lastOverlayWindowGeometry,
|
||||
shouldRefreshUnchangedGeometry: (geometry) =>
|
||||
shouldExitLinuxFullscreenOverrideForGeometry(geometry) ||
|
||||
(process.platform === 'linux' &&
|
||||
(hasLiveOverlayWindowBoundsMismatch(
|
||||
[overlayManager.getMainWindow(), overlayManager.getModalWindow()],
|
||||
geometry,
|
||||
) ||
|
||||
hasHyprlandOverlayWindowPlacementMismatch(geometry))),
|
||||
setOverlayWindowBounds: (geometry) => applyOverlayRegions(geometry),
|
||||
afterSetOverlayWindowBounds: () => {
|
||||
if (!overlayManager.getVisibleOverlayVisible()) {
|
||||
return;
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
deps.scheduleWindowsVisibleOverlayZOrderSyncBurst();
|
||||
return;
|
||||
}
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
if (process.platform === 'linux') {
|
||||
restoreLinuxOverlayWindowShape(mainWindow);
|
||||
}
|
||||
ensureOverlayWindowLevel(mainWindow);
|
||||
},
|
||||
});
|
||||
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
|
||||
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
||||
updateVisibleOverlayBoundsMainDeps,
|
||||
);
|
||||
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler({
|
||||
getCurrentOverlayWindowBounds: () => lastOverlayWindowGeometry,
|
||||
shouldRefreshUnchangedGeometry: (geometry) =>
|
||||
shouldExitLinuxFullscreenOverrideForGeometry(geometry) ||
|
||||
(process.platform === 'linux' &&
|
||||
(hasLiveOverlayWindowBoundsMismatch(
|
||||
[overlayManager.getMainWindow(), overlayManager.getModalWindow()],
|
||||
geometry,
|
||||
) ||
|
||||
hasHyprlandOverlayWindowPlacementMismatch(geometry))),
|
||||
setOverlayWindowBounds: (geometry) => applyOverlayRegions(geometry),
|
||||
afterSetOverlayWindowBounds: () => {
|
||||
if (!overlayManager.getVisibleOverlayVisible()) {
|
||||
return;
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
deps.scheduleWindowsVisibleOverlayZOrderSyncBurst();
|
||||
return;
|
||||
}
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
if (process.platform === 'linux') {
|
||||
restoreLinuxOverlayWindowShape(mainWindow);
|
||||
}
|
||||
ensureOverlayWindowLevel(mainWindow);
|
||||
},
|
||||
});
|
||||
|
||||
const buildEnsureOverlayWindowLevelMainDepsHandler =
|
||||
createBuildEnsureOverlayWindowLevelMainDepsHandler({
|
||||
shouldSuppressOverlayWindowLevel: (window) => {
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
return (
|
||||
(deps.getStatsOverlayVisible() && window === mainWindow) ||
|
||||
shouldSuppressVisibleOverlayRaiseForSeparateWindow({
|
||||
window,
|
||||
mainWindow,
|
||||
separateWindows: deps.getOverlayForegroundSeparateWindows(),
|
||||
})
|
||||
);
|
||||
},
|
||||
ensureOverlayWindowLevelCore: (window) =>
|
||||
ensureOverlayWindowLevelCore(window as BrowserWindow),
|
||||
afterEnsureOverlayWindowLevel: () => {
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
moveVisibleOverlayAboveTrackedPlaybackWindow(mainWindow);
|
||||
}
|
||||
promoteStatsOverlayAbovePlayback();
|
||||
},
|
||||
});
|
||||
const ensureOverlayWindowLevelMainDeps = buildEnsureOverlayWindowLevelMainDepsHandler();
|
||||
const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler(
|
||||
ensureOverlayWindowLevelMainDeps,
|
||||
);
|
||||
const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler({
|
||||
shouldSuppressOverlayWindowLevel: (window) => {
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
return (
|
||||
(deps.getStatsOverlayVisible() && window === mainWindow) ||
|
||||
shouldSuppressVisibleOverlayRaiseForSeparateWindow({
|
||||
window,
|
||||
mainWindow,
|
||||
separateWindows: deps.getOverlayForegroundSeparateWindows(),
|
||||
})
|
||||
);
|
||||
},
|
||||
ensureOverlayWindowLevelCore: (window) => ensureOverlayWindowLevelCore(window as BrowserWindow),
|
||||
afterEnsureOverlayWindowLevel: () => {
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
moveVisibleOverlayAboveTrackedPlaybackWindow(mainWindow);
|
||||
}
|
||||
promoteStatsOverlayAbovePlayback();
|
||||
},
|
||||
});
|
||||
|
||||
function syncPrimaryOverlayWindowLayer(layer: 'visible'): void {
|
||||
const mainWindow = overlayManager.getMainWindow();
|
||||
@@ -276,23 +260,18 @@ export function createOverlayGeometryRuntime(deps: OverlayGeometryRuntimeDeps) {
|
||||
);
|
||||
}
|
||||
|
||||
const buildEnforceOverlayLayerOrderMainDepsHandler =
|
||||
createBuildEnforceOverlayLayerOrderMainDepsHandler({
|
||||
enforceOverlayLayerOrderCore: (params) =>
|
||||
enforceOverlayLayerOrderCore({
|
||||
visibleOverlayVisible: params.visibleOverlayVisible,
|
||||
mainWindow: params.mainWindow as BrowserWindow | null,
|
||||
ensureOverlayWindowLevel: (window) =>
|
||||
params.ensureOverlayWindowLevel(window as BrowserWindow),
|
||||
}),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window as BrowserWindow),
|
||||
});
|
||||
const enforceOverlayLayerOrderMainDeps = buildEnforceOverlayLayerOrderMainDepsHandler();
|
||||
const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler(
|
||||
enforceOverlayLayerOrderMainDeps,
|
||||
);
|
||||
const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler({
|
||||
enforceOverlayLayerOrderCore: (params) =>
|
||||
enforceOverlayLayerOrderCore({
|
||||
visibleOverlayVisible: params.visibleOverlayVisible,
|
||||
mainWindow: params.mainWindow as BrowserWindow | null,
|
||||
ensureOverlayWindowLevel: (window) =>
|
||||
params.ensureOverlayWindowLevel(window as BrowserWindow),
|
||||
}),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window as BrowserWindow),
|
||||
});
|
||||
|
||||
return {
|
||||
getLastOverlayWindowGeometry: () => lastOverlayWindowGeometry,
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import { createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler } from './app-runtime-main-deps';
|
||||
import { createInitializeOverlayRuntimeHandler } from './overlay-runtime-bootstrap';
|
||||
import { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options';
|
||||
import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-runtime-options-main-deps';
|
||||
|
||||
type InitializeOverlayRuntimeMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeMainDepsHandler
|
||||
>[0];
|
||||
type InitializeOverlayRuntimeOptions = ReturnType<
|
||||
ReturnType<typeof createBuildInitializeOverlayRuntimeOptionsHandler>
|
||||
>;
|
||||
type InitializeOverlayRuntimeBootstrapMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<InitializeOverlayRuntimeOptions>
|
||||
>[0];
|
||||
type InitializeOverlayRuntimeDeps = Parameters<typeof createInitializeOverlayRuntimeHandler>[0];
|
||||
|
||||
export function createOverlayRuntimeBootstrapHandlers(deps: {
|
||||
initializeOverlayRuntimeMainDeps: InitializeOverlayRuntimeMainDeps;
|
||||
initializeOverlayRuntimeBootstrapDeps: Omit<
|
||||
InitializeOverlayRuntimeBootstrapMainDeps,
|
||||
'buildOptions'
|
||||
>;
|
||||
initializeOverlayRuntimeMainDeps: Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeOptionsHandler
|
||||
>[0];
|
||||
initializeOverlayRuntimeBootstrapDeps: Omit<InitializeOverlayRuntimeDeps, 'buildOptions'>;
|
||||
}) {
|
||||
const buildInitializeOverlayRuntimeOptionsHandler =
|
||||
createBuildInitializeOverlayRuntimeOptionsHandler(
|
||||
createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(),
|
||||
);
|
||||
const initializeOverlayRuntime = createInitializeOverlayRuntimeHandler(
|
||||
createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler({
|
||||
...deps.initializeOverlayRuntimeBootstrapDeps,
|
||||
buildOptions: () => buildInitializeOverlayRuntimeOptionsHandler(),
|
||||
})(),
|
||||
const buildInitializeOverlayRuntimeOptions = createBuildInitializeOverlayRuntimeOptionsHandler(
|
||||
deps.initializeOverlayRuntimeMainDeps,
|
||||
);
|
||||
const initializeOverlayRuntime = createInitializeOverlayRuntimeHandler({
|
||||
...deps.initializeOverlayRuntimeBootstrapDeps,
|
||||
buildOptions: () => buildInitializeOverlayRuntimeOptions(),
|
||||
});
|
||||
|
||||
return {
|
||||
initializeOverlayRuntime,
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { AnkiConnectConfig } from '../../types';
|
||||
import type { OverlayNotificationPayload } from '../../types/notification';
|
||||
import type { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options';
|
||||
|
||||
type OverlayRuntimeOptionsMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeOptionsHandler
|
||||
>[0];
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
appState: {
|
||||
backendOverride: string | null;
|
||||
windowTracker: Parameters<OverlayRuntimeOptionsMainDeps['setWindowTracker']>[0];
|
||||
subtitleTimingTracker: ReturnType<OverlayRuntimeOptionsMainDeps['getSubtitleTimingTracker']>;
|
||||
mpvClient: ReturnType<OverlayRuntimeOptionsMainDeps['getMpvClient']>;
|
||||
mpvSocketPath: string;
|
||||
runtimeOptionsManager: ReturnType<OverlayRuntimeOptionsMainDeps['getRuntimeOptionsManager']>;
|
||||
ankiIntegration: Parameters<OverlayRuntimeOptionsMainDeps['setAnkiIntegration']>[0];
|
||||
};
|
||||
overlayManager: {
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
};
|
||||
overlayVisibilityRuntime: {
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
};
|
||||
refreshCurrentSubtitle?: () => void;
|
||||
overlayShortcutsRuntime: {
|
||||
syncOverlayShortcuts: () => void;
|
||||
};
|
||||
createMainWindow: () => void;
|
||||
registerGlobalShortcuts: () => void;
|
||||
updateVisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => void;
|
||||
getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows'];
|
||||
createWindowTracker?: OverlayRuntimeOptionsMainDeps['createWindowTracker'];
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback'];
|
||||
getKnownWordCacheStatePath: () => string;
|
||||
getCachedMediaPath?: OverlayRuntimeOptionsMainDeps['getCachedMediaPath'];
|
||||
shouldRequireRemoteMediaCache?: OverlayRuntimeOptionsMainDeps['shouldRequireRemoteMediaCache'];
|
||||
getYoutubeMediaSourceUrl?: OverlayRuntimeOptionsMainDeps['getYoutubeMediaSourceUrl'];
|
||||
shouldStartAnkiIntegration: () => boolean;
|
||||
bindOverlayOwner?: () => void;
|
||||
releaseOverlayOwner?: () => void;
|
||||
}) {
|
||||
return (): OverlayRuntimeOptionsMainDeps => ({
|
||||
getBackendOverride: () => deps.appState.backendOverride,
|
||||
createMainWindow: () => deps.createMainWindow(),
|
||||
registerGlobalShortcuts: () => deps.registerGlobalShortcuts(),
|
||||
updateVisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => deps.updateVisibleOverlayBounds(geometry),
|
||||
isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () =>
|
||||
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
refreshCurrentSubtitle: () => deps.refreshCurrentSubtitle?.(),
|
||||
getOverlayWindows: () => deps.getOverlayWindows(),
|
||||
syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||
setWindowTracker: (tracker) => {
|
||||
deps.appState.windowTracker = tracker;
|
||||
},
|
||||
createWindowTracker: deps.createWindowTracker,
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
getSubtitleTimingTracker: () => deps.appState.subtitleTimingTracker,
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
getMpvSocketPath: () => deps.appState.mpvSocketPath,
|
||||
getRuntimeOptionsManager: () => deps.appState.runtimeOptionsManager,
|
||||
setAnkiIntegration: (integration) => {
|
||||
deps.appState.ankiIntegration = integration;
|
||||
},
|
||||
showDesktopNotification: deps.showDesktopNotification,
|
||||
showOverlayNotification: deps.showOverlayNotification,
|
||||
dismissOverlayNotification: deps.dismissOverlayNotification,
|
||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
|
||||
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
||||
...(deps.shouldRequireRemoteMediaCache
|
||||
? { shouldRequireRemoteMediaCache: deps.shouldRequireRemoteMediaCache }
|
||||
: {}),
|
||||
...(deps.getYoutubeMediaSourceUrl
|
||||
? { getYoutubeMediaSourceUrl: deps.getYoutubeMediaSourceUrl }
|
||||
: {}),
|
||||
shouldStartAnkiIntegration: () => deps.shouldStartAnkiIntegration(),
|
||||
bindOverlayOwner: deps.bindOverlayOwner,
|
||||
releaseOverlayOwner: deps.releaseOverlayOwner,
|
||||
});
|
||||
}
|
||||
@@ -49,69 +49,75 @@ type OverlayRuntimeOptions = {
|
||||
releaseOverlayOwner?: () => void;
|
||||
};
|
||||
|
||||
// Builds the options for core initializeOverlayRuntime from main-process state. appState
|
||||
// fields are read when the options are built (or lazily via getters), and the window tracker
|
||||
// and Anki integration produced by the core are written back to appState.
|
||||
export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||
getBackendOverride: () => string | null;
|
||||
appState: {
|
||||
backendOverride: string | null;
|
||||
windowTracker: Parameters<OverlayRuntimeOptions['setWindowTracker']>[0];
|
||||
subtitleTimingTracker: ReturnType<OverlayRuntimeOptions['getSubtitleTimingTracker']>;
|
||||
mpvClient: ReturnType<OverlayRuntimeOptions['getMpvClient']>;
|
||||
mpvSocketPath: string;
|
||||
runtimeOptionsManager: ReturnType<OverlayRuntimeOptions['getRuntimeOptionsManager']>;
|
||||
ankiIntegration: Parameters<OverlayRuntimeOptions['setAnkiIntegration']>[0];
|
||||
};
|
||||
overlayManager: {
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
};
|
||||
overlayVisibilityRuntime: {
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
};
|
||||
refreshCurrentSubtitle?: () => void;
|
||||
overlayShortcutsRuntime: {
|
||||
syncOverlayShortcuts: () => void;
|
||||
};
|
||||
createMainWindow: () => void;
|
||||
registerGlobalShortcuts: () => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
refreshCurrentSubtitle?: () => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
syncOverlayShortcuts: () => void;
|
||||
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||
createWindowTracker?: (
|
||||
override?: string | null,
|
||||
targetMpvSocketPath?: string | null,
|
||||
) => BaseWindowTracker | null;
|
||||
createWindowTracker?: OverlayRuntimeOptions['createWindowTracker'];
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
getSubtitleTimingTracker: () => unknown | null;
|
||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||
getMpvSocketPath: () => string;
|
||||
getRuntimeOptionsManager: () => {
|
||||
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||
} | null;
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
showDesktopNotification: OverlayRuntimeOptions['showDesktopNotification'];
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
dismissOverlayNotification?: (id: string) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
createFieldGroupingCallback: OverlayRuntimeOptions['createFieldGroupingCallback'];
|
||||
getKnownWordCacheStatePath: () => string;
|
||||
getCachedMediaPath?: (
|
||||
currentVideoPath: string,
|
||||
kind: 'audio' | 'video',
|
||||
) => Promise<string | null>;
|
||||
getCachedMediaPath?: OverlayRuntimeOptions['getCachedMediaPath'];
|
||||
shouldRequireRemoteMediaCache?: () => boolean;
|
||||
getYoutubeMediaSourceUrl?: () => Promise<string | null | undefined> | string | null | undefined;
|
||||
getYoutubeMediaSourceUrl?: OverlayRuntimeOptions['getYoutubeMediaSourceUrl'];
|
||||
shouldStartAnkiIntegration: () => boolean;
|
||||
bindOverlayOwner?: () => void;
|
||||
releaseOverlayOwner?: () => void;
|
||||
}) {
|
||||
return (): OverlayRuntimeOptions => ({
|
||||
backendOverride: deps.getBackendOverride(),
|
||||
createMainWindow: deps.createMainWindow,
|
||||
registerGlobalShortcuts: deps.registerGlobalShortcuts,
|
||||
updateVisibleOverlayBounds: deps.updateVisibleOverlayBounds,
|
||||
isVisibleOverlayVisible: deps.isVisibleOverlayVisible,
|
||||
updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility,
|
||||
refreshCurrentSubtitle: deps.refreshCurrentSubtitle,
|
||||
getOverlayWindows: deps.getOverlayWindows,
|
||||
syncOverlayShortcuts: deps.syncOverlayShortcuts,
|
||||
setWindowTracker: deps.setWindowTracker,
|
||||
backendOverride: deps.appState.backendOverride,
|
||||
createMainWindow: () => deps.createMainWindow(),
|
||||
registerGlobalShortcuts: () => deps.registerGlobalShortcuts(),
|
||||
updateVisibleOverlayBounds: (geometry) => deps.updateVisibleOverlayBounds(geometry),
|
||||
isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () =>
|
||||
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
refreshCurrentSubtitle: () => deps.refreshCurrentSubtitle?.(),
|
||||
getOverlayWindows: () => deps.getOverlayWindows(),
|
||||
syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||
setWindowTracker: (tracker) => {
|
||||
deps.appState.windowTracker = tracker;
|
||||
},
|
||||
createWindowTracker: deps.createWindowTracker,
|
||||
getResolvedConfig: deps.getResolvedConfig,
|
||||
getSubtitleTimingTracker: deps.getSubtitleTimingTracker,
|
||||
getMpvClient: deps.getMpvClient,
|
||||
getMpvSocketPath: deps.getMpvSocketPath,
|
||||
getRuntimeOptionsManager: deps.getRuntimeOptionsManager,
|
||||
setAnkiIntegration: deps.setAnkiIntegration,
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
getSubtitleTimingTracker: () => deps.appState.subtitleTimingTracker,
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
getMpvSocketPath: () => deps.appState.mpvSocketPath,
|
||||
getRuntimeOptionsManager: () => deps.appState.runtimeOptionsManager,
|
||||
setAnkiIntegration: (integration) => {
|
||||
deps.appState.ankiIntegration = integration;
|
||||
},
|
||||
showDesktopNotification: deps.showDesktopNotification,
|
||||
showOverlayNotification: deps.showOverlayNotification,
|
||||
dismissOverlayNotification: deps.dismissOverlayNotification,
|
||||
createFieldGroupingCallback: deps.createFieldGroupingCallback,
|
||||
getKnownWordCacheStatePath: deps.getKnownWordCacheStatePath,
|
||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
|
||||
...(deps.getCachedMediaPath ? { getCachedMediaPath: deps.getCachedMediaPath } : {}),
|
||||
...(deps.shouldRequireRemoteMediaCache
|
||||
? { shouldRequireRemoteMediaCache: deps.shouldRequireRemoteMediaCache }
|
||||
@@ -119,7 +125,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||
...(deps.getYoutubeMediaSourceUrl
|
||||
? { getYoutubeMediaSourceUrl: deps.getYoutubeMediaSourceUrl }
|
||||
: {}),
|
||||
shouldStartAnkiIntegration: deps.shouldStartAnkiIntegration,
|
||||
shouldStartAnkiIntegration: () => deps.shouldStartAnkiIntegration(),
|
||||
bindOverlayOwner: deps.bindOverlayOwner,
|
||||
releaseOverlayOwner: deps.releaseOverlayOwner,
|
||||
});
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { OverlayShortcutRuntimeServiceInput } from '../overlay-shortcuts-runtime';
|
||||
|
||||
export function createBuildOverlayShortcutsRuntimeMainDepsHandler(
|
||||
deps: OverlayShortcutRuntimeServiceInput,
|
||||
) {
|
||||
return (): OverlayShortcutRuntimeServiceInput => ({
|
||||
getConfiguredShortcuts: () => deps.getConfiguredShortcuts(),
|
||||
getShortcutsRegistered: () => deps.getShortcutsRegistered(),
|
||||
setShortcutsRegistered: (registered: boolean) => deps.setShortcutsRegistered(registered),
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
isOverlayShortcutContextActive: () => deps.isOverlayShortcutContextActive?.() ?? true,
|
||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||
openCharacterDictionaryManager: () => deps.openCharacterDictionaryManager(),
|
||||
openJimaku: () => deps.openJimaku(),
|
||||
openTsukihime: () => deps.openTsukihime(),
|
||||
markAudioCard: () => deps.markAudioCard(),
|
||||
copySubtitleMultiple: (timeoutMs: number) => deps.copySubtitleMultiple(timeoutMs),
|
||||
copySubtitle: () => deps.copySubtitle(),
|
||||
toggleSecondarySubMode: () => deps.toggleSecondarySubMode(),
|
||||
updateLastCardFromClipboard: () => deps.updateLastCardFromClipboard(),
|
||||
triggerFieldGrouping: () => deps.triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||
mineSentenceCard: () => deps.mineSentenceCard(),
|
||||
mineSentenceMultiple: (timeoutMs: number) => deps.mineSentenceMultiple(timeoutMs),
|
||||
cancelPendingMultiCopy: () => deps.cancelPendingMultiCopy(),
|
||||
cancelPendingMineSentenceMultiple: () => deps.cancelPendingMineSentenceMultiple(),
|
||||
});
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type {
|
||||
createSetVisibleOverlayVisibleHandler,
|
||||
createToggleVisibleOverlayHandler,
|
||||
} from './overlay-visibility-actions';
|
||||
|
||||
type SetVisibleOverlayVisibleMainDeps = Parameters<typeof createSetVisibleOverlayVisibleHandler>[0];
|
||||
type ToggleVisibleOverlayMainDeps = Parameters<typeof createToggleVisibleOverlayHandler>[0];
|
||||
|
||||
export function createBuildSetVisibleOverlayVisibleMainDepsHandler(
|
||||
deps: SetVisibleOverlayVisibleMainDeps,
|
||||
) {
|
||||
return (): SetVisibleOverlayVisibleMainDeps => ({
|
||||
getVisibleOverlayVisible: deps.getVisibleOverlayVisible
|
||||
? () => deps.getVisibleOverlayVisible?.() ?? false
|
||||
: undefined,
|
||||
setVisibleOverlayVisibleCore: (options) => deps.setVisibleOverlayVisibleCore(options),
|
||||
setVisibleOverlayVisibleState: (visible: boolean) =>
|
||||
deps.setVisibleOverlayVisibleState(visible),
|
||||
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(),
|
||||
onVisibleOverlayEnabled: deps.onVisibleOverlayEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildToggleVisibleOverlayMainDepsHandler(deps: ToggleVisibleOverlayMainDeps) {
|
||||
return (): ToggleVisibleOverlayMainDeps => ({
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||
});
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { WindowGeometry } from '../../types';
|
||||
import type { OverlayVisibilityRuntimeDeps } from '../overlay-visibility-runtime';
|
||||
|
||||
export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
|
||||
deps: OverlayVisibilityRuntimeDeps,
|
||||
) {
|
||||
return (): OverlayVisibilityRuntimeDeps => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getModalActive: () => deps.getModalActive(),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getForceMousePassthrough: () => deps.getForceMousePassthrough(),
|
||||
getNonNativeInputRegionActive: () => deps.getNonNativeInputRegionActive?.() ?? false,
|
||||
getSuspendVisibleOverlay: () => deps.getSuspendVisibleOverlay?.() ?? false,
|
||||
getOverlayInteractionActive: () => deps.getOverlayInteractionActive?.() ?? false,
|
||||
getWindowTracker: () => deps.getWindowTracker(),
|
||||
getMacOSForegroundProbeActive: () => deps.getMacOSForegroundProbeActive?.() ?? false,
|
||||
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.updateVisibleOverlayBounds(geometry),
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
|
||||
syncWindowsOverlayToMpvZOrder: (window: BrowserWindow) =>
|
||||
deps.syncWindowsOverlayToMpvZOrder?.(window),
|
||||
syncPrimaryOverlayWindowLayer: (layer: 'visible') => deps.syncPrimaryOverlayWindowLayer(layer),
|
||||
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
|
||||
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
|
||||
isMacOSPlatform: () => deps.isMacOSPlatform(),
|
||||
isWindowsPlatform: () => deps.isWindowsPlatform(),
|
||||
showOverlayLoadingOsd: (message: string) => deps.showOverlayLoadingOsd(message),
|
||||
dismissOverlayLoadingOsd: () => deps.dismissOverlayLoadingOsd?.(),
|
||||
hideNonNativeOverlayWhenTargetUnfocused: () =>
|
||||
deps.hideNonNativeOverlayWhenTargetUnfocused?.() ?? false,
|
||||
resolveFallbackBounds: () => deps.resolveFallbackBounds(),
|
||||
});
|
||||
}
|
||||
@@ -2,35 +2,25 @@ import {
|
||||
createSetVisibleOverlayVisibleHandler,
|
||||
createToggleVisibleOverlayHandler,
|
||||
} from './overlay-visibility-actions';
|
||||
import {
|
||||
createBuildSetVisibleOverlayVisibleMainDepsHandler,
|
||||
createBuildToggleVisibleOverlayMainDepsHandler,
|
||||
} from './overlay-visibility-actions-main-deps';
|
||||
import { createSetOverlayVisibleHandler, createToggleOverlayHandler } from './overlay-main-actions';
|
||||
|
||||
type SetVisibleOverlayVisibleMainDeps = Parameters<
|
||||
typeof createBuildSetVisibleOverlayVisibleMainDepsHandler
|
||||
>[0];
|
||||
type SetVisibleOverlayVisibleDeps = Parameters<typeof createSetVisibleOverlayVisibleHandler>[0];
|
||||
|
||||
export type OverlayVisibilityRuntimeDeps = {
|
||||
setVisibleOverlayVisibleDeps: SetVisibleOverlayVisibleMainDeps;
|
||||
setVisibleOverlayVisibleDeps: SetVisibleOverlayVisibleDeps;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
};
|
||||
|
||||
export function createOverlayVisibilityRuntime(deps: OverlayVisibilityRuntimeDeps) {
|
||||
const setVisibleOverlayVisibleMainDeps = createBuildSetVisibleOverlayVisibleMainDepsHandler({
|
||||
const setVisibleOverlayVisible = createSetVisibleOverlayVisibleHandler({
|
||||
...deps.setVisibleOverlayVisibleDeps,
|
||||
getVisibleOverlayVisible: deps.getVisibleOverlayVisible,
|
||||
})();
|
||||
const setVisibleOverlayVisible = createSetVisibleOverlayVisibleHandler(
|
||||
setVisibleOverlayVisibleMainDeps,
|
||||
);
|
||||
});
|
||||
|
||||
const toggleVisibleOverlayMainDeps = createBuildToggleVisibleOverlayMainDepsHandler({
|
||||
const toggleVisibleOverlay = createToggleVisibleOverlayHandler({
|
||||
getVisibleOverlayVisible: deps.getVisibleOverlayVisible,
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
})();
|
||||
const toggleVisibleOverlay = createToggleVisibleOverlayHandler(toggleVisibleOverlayMainDeps);
|
||||
});
|
||||
|
||||
const setOverlayVisible = createSetOverlayVisibleHandler({
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { Session } from 'electron';
|
||||
|
||||
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindowCore: (
|
||||
kind: 'visible' | 'modal',
|
||||
options: {
|
||||
isDev: boolean;
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'modal') => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
forwardTabToMpv: () => void;
|
||||
linuxX11FullscreenOverlay?: boolean;
|
||||
onVisibleWindowBlurred?: () => void;
|
||||
onVisibleWindowFocused?: () => void;
|
||||
onWindowDidFinishLoad?: () => void;
|
||||
onWindowContentReady?: () => void;
|
||||
onWindowClosed: (windowKind: 'visible' | 'modal', window: TWindow) => void;
|
||||
yomitanSession?: Session | null;
|
||||
},
|
||||
) => TWindow;
|
||||
isDev: boolean;
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: 'visible' | 'modal') => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
forwardTabToMpv: () => void;
|
||||
getLinuxX11FullscreenOverlay?: () => boolean;
|
||||
onVisibleWindowBlurred?: () => void;
|
||||
onVisibleWindowFocused?: () => void;
|
||||
onWindowDidFinishLoad?: () => void;
|
||||
onWindowContentReady?: () => void;
|
||||
onWindowClosed: (windowKind: 'visible' | 'modal', window: TWindow) => void;
|
||||
getYomitanSession?: () => Session | null;
|
||||
}) {
|
||||
return () => ({
|
||||
createOverlayWindowCore: deps.createOverlayWindowCore,
|
||||
isDev: deps.isDev,
|
||||
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
|
||||
onRuntimeOptionsChanged: deps.onRuntimeOptionsChanged,
|
||||
setOverlayDebugVisualizationEnabled: deps.setOverlayDebugVisualizationEnabled,
|
||||
isOverlayVisible: deps.isOverlayVisible,
|
||||
tryHandleOverlayShortcutLocalFallback: deps.tryHandleOverlayShortcutLocalFallback,
|
||||
forwardTabToMpv: deps.forwardTabToMpv,
|
||||
getLinuxX11FullscreenOverlay: deps.getLinuxX11FullscreenOverlay,
|
||||
onVisibleWindowBlurred: deps.onVisibleWindowBlurred,
|
||||
onVisibleWindowFocused: deps.onVisibleWindowFocused,
|
||||
onWindowDidFinishLoad: deps.onWindowDidFinishLoad,
|
||||
onWindowContentReady: deps.onWindowContentReady,
|
||||
onWindowClosed: deps.onWindowClosed,
|
||||
getYomitanSession: () => deps.getYomitanSession?.() ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
||||
getMainWindow: () => TWindow | null;
|
||||
isWindowDestroyed: (window: TWindow) => boolean;
|
||||
createOverlayWindow: (kind: 'visible' | 'modal') => TWindow;
|
||||
setMainWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
isWindowDestroyed: (window: TWindow) => deps.isWindowDestroyed(window),
|
||||
createOverlayWindow: deps.createOverlayWindow,
|
||||
setMainWindow: deps.setMainWindow,
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildCreateModalWindowMainDepsHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: 'visible' | 'modal') => TWindow;
|
||||
setModalWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createOverlayWindow: deps.createOverlayWindow,
|
||||
setModalWindow: deps.setModalWindow,
|
||||
});
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type {
|
||||
createEnforceOverlayLayerOrderHandler,
|
||||
createEnsureOverlayWindowLevelHandler,
|
||||
createUpdateVisibleOverlayBoundsHandler,
|
||||
} from './overlay-window-layout';
|
||||
|
||||
type UpdateVisibleOverlayBoundsMainDeps = Parameters<
|
||||
typeof createUpdateVisibleOverlayBoundsHandler
|
||||
>[0];
|
||||
type EnsureOverlayWindowLevelMainDeps = Parameters<typeof createEnsureOverlayWindowLevelHandler>[0];
|
||||
type EnforceOverlayLayerOrderMainDeps = Parameters<typeof createEnforceOverlayLayerOrderHandler>[0];
|
||||
|
||||
export function createBuildUpdateVisibleOverlayBoundsMainDepsHandler(
|
||||
deps: UpdateVisibleOverlayBoundsMainDeps,
|
||||
) {
|
||||
return (): UpdateVisibleOverlayBoundsMainDeps => ({
|
||||
getCurrentOverlayWindowBounds: () => deps.getCurrentOverlayWindowBounds?.() ?? null,
|
||||
shouldRefreshUnchangedGeometry: (geometry) =>
|
||||
deps.shouldRefreshUnchangedGeometry?.(geometry) ?? false,
|
||||
setOverlayWindowBounds: (geometry) => deps.setOverlayWindowBounds(geometry),
|
||||
afterSetOverlayWindowBounds: (geometry) => deps.afterSetOverlayWindowBounds?.(geometry),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildEnsureOverlayWindowLevelMainDepsHandler(
|
||||
deps: EnsureOverlayWindowLevelMainDeps,
|
||||
) {
|
||||
return (): EnsureOverlayWindowLevelMainDeps => ({
|
||||
shouldSuppressOverlayWindowLevel: (window: unknown) =>
|
||||
deps.shouldSuppressOverlayWindowLevel?.(window) ?? false,
|
||||
ensureOverlayWindowLevelCore: (window: unknown) => deps.ensureOverlayWindowLevelCore(window),
|
||||
afterEnsureOverlayWindowLevel: (window: unknown) =>
|
||||
deps.afterEnsureOverlayWindowLevel?.(window),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildEnforceOverlayLayerOrderMainDepsHandler(
|
||||
deps: EnforceOverlayLayerOrderMainDeps,
|
||||
) {
|
||||
return (): EnforceOverlayLayerOrderMainDeps => ({
|
||||
enforceOverlayLayerOrderCore: (params) => deps.enforceOverlayLayerOrderCore(params),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
ensureOverlayWindowLevel: (window: unknown) => deps.ensureOverlayWindowLevel(window),
|
||||
});
|
||||
}
|
||||
@@ -3,40 +3,31 @@ import {
|
||||
createCreateModalWindowHandler,
|
||||
createCreateOverlayWindowHandler,
|
||||
} from './overlay-window-factory';
|
||||
import {
|
||||
createBuildCreateMainWindowMainDepsHandler,
|
||||
createBuildCreateModalWindowMainDepsHandler,
|
||||
createBuildCreateOverlayWindowMainDepsHandler,
|
||||
} from './overlay-window-factory-main-deps';
|
||||
|
||||
type CreateOverlayWindowMainDeps<TWindow> = Parameters<
|
||||
typeof createBuildCreateOverlayWindowMainDepsHandler<TWindow>
|
||||
type CreateOverlayWindowDeps<TWindow> = Parameters<
|
||||
typeof createCreateOverlayWindowHandler<TWindow>
|
||||
>[0];
|
||||
|
||||
export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
||||
createOverlayWindowDeps: CreateOverlayWindowMainDeps<TWindow>;
|
||||
createOverlayWindowDeps: CreateOverlayWindowDeps<TWindow>;
|
||||
getMainWindow: () => TWindow | null;
|
||||
isWindowDestroyed: (window: TWindow) => boolean;
|
||||
setMainWindow: (window: TWindow | null) => void;
|
||||
setModalWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
||||
createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps.createOverlayWindowDeps)(),
|
||||
);
|
||||
const createMainWindow = createCreateMainWindowHandler<TWindow>(
|
||||
createBuildCreateMainWindowMainDepsHandler<TWindow>({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
isWindowDestroyed: (window) => deps.isWindowDestroyed(window),
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setMainWindow: (window) => deps.setMainWindow(window),
|
||||
})(),
|
||||
);
|
||||
const createModalWindow = createCreateModalWindowHandler<TWindow>(
|
||||
createBuildCreateModalWindowMainDepsHandler<TWindow>({
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setModalWindow: (window) => deps.setModalWindow(window),
|
||||
})(),
|
||||
deps.createOverlayWindowDeps,
|
||||
);
|
||||
const createMainWindow = createCreateMainWindowHandler<TWindow>({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
isWindowDestroyed: (window) => deps.isWindowDestroyed(window),
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setMainWindow: (window) => deps.setMainWindow(window),
|
||||
});
|
||||
const createModalWindow = createCreateModalWindowHandler<TWindow>({
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setModalWindow: (window) => deps.setModalWindow(window),
|
||||
});
|
||||
|
||||
return {
|
||||
createOverlayWindow,
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { CliArgs } from '../../cli/args';
|
||||
import type { ResolvedConfig } from '../../types';
|
||||
import type { LogLevelSource } from '../../logger';
|
||||
import type { StartupBootstrapRuntimeFactoryDeps } from '../startup';
|
||||
|
||||
export function createBuildStartupBootstrapRuntimeFactoryDepsHandler(
|
||||
deps: StartupBootstrapRuntimeFactoryDeps,
|
||||
) {
|
||||
return (): StartupBootstrapRuntimeFactoryDeps => ({
|
||||
argv: deps.argv,
|
||||
parseArgs: deps.parseArgs,
|
||||
setLogLevel: deps.setLogLevel,
|
||||
forceX11Backend: deps.forceX11Backend,
|
||||
enforceUnsupportedWaylandMode: deps.enforceUnsupportedWaylandMode,
|
||||
shouldStartApp: deps.shouldStartApp,
|
||||
getDefaultSocketPath: deps.getDefaultSocketPath,
|
||||
defaultTexthookerPort: deps.defaultTexthookerPort,
|
||||
configDir: deps.configDir,
|
||||
defaultConfig: deps.defaultConfig,
|
||||
generateConfigTemplate: deps.generateConfigTemplate,
|
||||
generateDefaultConfigFile: deps.generateDefaultConfigFile,
|
||||
onConfigGenerated: deps.onConfigGenerated,
|
||||
onGenerateConfigError: deps.onGenerateConfigError,
|
||||
startAppLifecycle: deps.startAppLifecycle,
|
||||
});
|
||||
}
|
||||
|
||||
export type {
|
||||
CliArgs as StartupBuilderCliArgs,
|
||||
ResolvedConfig as StartupBuilderResolvedConfig,
|
||||
LogLevelSource as StartupBuilderLogLevelSource,
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildStartupBootstrapMainDepsHandler } from './startup-bootstrap-main-deps';
|
||||
|
||||
function buildDeps() {
|
||||
const state = { exitCodes: [] as number[], quitCount: 0, errors: [] as string[] };
|
||||
const deps = createBuildStartupBootstrapMainDepsHandler({
|
||||
argv: ['node', 'main.js'],
|
||||
parseArgs: () => ({}) as never,
|
||||
setLogLevel: () => {},
|
||||
forceX11Backend: () => {},
|
||||
enforceUnsupportedWaylandMode: () => {},
|
||||
shouldStartApp: () => true,
|
||||
getDefaultSocketPath: () => '/tmp/mpv.sock',
|
||||
defaultTexthookerPort: 5174,
|
||||
configDir: '/tmp/config',
|
||||
defaultConfig: {} as never,
|
||||
generateConfigTemplate: () => 'template',
|
||||
generateDefaultConfigFile: async () => 0,
|
||||
setExitCode: (code) => state.exitCodes.push(code),
|
||||
quitApp: () => {
|
||||
state.quitCount += 1;
|
||||
},
|
||||
logGenerateConfigError: (message) => state.errors.push(message),
|
||||
startAppLifecycle: () => {},
|
||||
})();
|
||||
return { deps, state };
|
||||
}
|
||||
|
||||
test('onConfigGenerated exits with the generator exit code', () => {
|
||||
const { deps, state } = buildDeps();
|
||||
deps.onConfigGenerated(7);
|
||||
assert.deepEqual(state.exitCodes, [7]);
|
||||
assert.equal(state.quitCount, 1);
|
||||
assert.deepEqual(state.errors, []);
|
||||
});
|
||||
|
||||
test('onGenerateConfigError logs the failure and exits with code 1', () => {
|
||||
const { deps, state } = buildDeps();
|
||||
deps.onGenerateConfigError(new Error('boom'));
|
||||
assert.deepEqual(state.errors, ['Failed to generate config: boom']);
|
||||
assert.deepEqual(state.exitCodes, [1]);
|
||||
assert.equal(state.quitCount, 1);
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
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),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { StartupBootstrapRuntimeFactoryDeps } from '../startup';
|
||||
import { createStartupRuntimeHandlers } from './startup-runtime-handlers';
|
||||
|
||||
function buildBootstrapDeps() {
|
||||
const state = { exitCodes: [] as number[], quitCount: 0, errors: [] as string[] };
|
||||
const captured: StartupBootstrapRuntimeFactoryDeps[] = [];
|
||||
const { runAndApplyStartupState } = createStartupRuntimeHandlers({
|
||||
appLifecycleRuntimeRunnerMainDeps: {} as never,
|
||||
createAppLifecycleRuntimeRunner: () => () => {},
|
||||
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
|
||||
argv: ['node', 'main.js'],
|
||||
parseArgs: () => ({}) as never,
|
||||
setLogLevel: () => {},
|
||||
forceX11Backend: () => {},
|
||||
enforceUnsupportedWaylandMode: () => {},
|
||||
shouldStartApp: () => true,
|
||||
getDefaultSocketPath: () => '/tmp/mpv.sock',
|
||||
defaultTexthookerPort: 5174,
|
||||
configDir: '/tmp/config',
|
||||
defaultConfig: {} as never,
|
||||
generateConfigTemplate: () => 'template',
|
||||
generateDefaultConfigFile: async () => 0,
|
||||
setExitCode: (code) => state.exitCodes.push(code),
|
||||
quitApp: () => {
|
||||
state.quitCount += 1;
|
||||
},
|
||||
logGenerateConfigError: (message) => state.errors.push(message),
|
||||
startAppLifecycle,
|
||||
}),
|
||||
createStartupBootstrapRuntimeDeps: (deps) => deps,
|
||||
runStartupBootstrapRuntime: (deps) => {
|
||||
captured.push(deps);
|
||||
},
|
||||
applyStartupState: () => {},
|
||||
});
|
||||
runAndApplyStartupState();
|
||||
const [deps] = captured;
|
||||
assert.ok(deps);
|
||||
return { deps, state };
|
||||
}
|
||||
|
||||
test('onConfigGenerated exits with the generator exit code', () => {
|
||||
const { deps, state } = buildBootstrapDeps();
|
||||
deps.onConfigGenerated(7);
|
||||
assert.deepEqual(state.exitCodes, [7]);
|
||||
assert.equal(state.quitCount, 1);
|
||||
assert.deepEqual(state.errors, []);
|
||||
});
|
||||
|
||||
test('onGenerateConfigError logs the failure and exits with code 1', () => {
|
||||
const { deps, state } = buildBootstrapDeps();
|
||||
deps.onGenerateConfigError(new Error('boom'));
|
||||
assert.deepEqual(state.errors, ['Failed to generate config: boom']);
|
||||
assert.deepEqual(state.exitCodes, [1]);
|
||||
assert.equal(state.quitCount, 1);
|
||||
});
|
||||
@@ -1,11 +1,15 @@
|
||||
import { createBuildStartupBootstrapRuntimeFactoryDepsHandler } from './startup-bootstrap-deps-builder';
|
||||
import type { AppLifecycleRuntimeRunnerParams } from '../startup-lifecycle';
|
||||
import { createBuildStartupBootstrapMainDepsHandler } from './startup-bootstrap-main-deps';
|
||||
import type { StartupBootstrapRuntimeFactoryDeps } from '../startup';
|
||||
|
||||
type StartupBootstrapMainDeps = Parameters<typeof createBuildStartupBootstrapMainDepsHandler>[0];
|
||||
type StartupBootstrapRuntimeFactoryDeps = Parameters<
|
||||
typeof createBuildStartupBootstrapRuntimeFactoryDepsHandler
|
||||
>[0];
|
||||
// Bootstrap deps as main.ts supplies them; the config-generation exit callbacks are built here.
|
||||
type StartupBootstrapMainDeps = Omit<
|
||||
StartupBootstrapRuntimeFactoryDeps,
|
||||
'onConfigGenerated' | 'onGenerateConfigError'
|
||||
> & {
|
||||
setExitCode: (code: number) => void;
|
||||
quitApp: () => void;
|
||||
logGenerateConfigError: (message: string) => void;
|
||||
};
|
||||
|
||||
export function createStartupRuntimeHandlers<
|
||||
TCliArgs,
|
||||
@@ -29,11 +33,20 @@ export function createStartupRuntimeHandlers<
|
||||
deps.appLifecycleRuntimeRunnerMainDeps,
|
||||
);
|
||||
|
||||
const startupBootstrapMainDeps = createBuildStartupBootstrapMainDepsHandler(
|
||||
deps.buildStartupBootstrapMainDeps(appLifecycleRuntimeRunner),
|
||||
)();
|
||||
const startupBootstrapRuntimeFactoryDeps =
|
||||
createBuildStartupBootstrapRuntimeFactoryDepsHandler(startupBootstrapMainDeps)();
|
||||
const { setExitCode, quitApp, logGenerateConfigError, ...bootstrapDeps } =
|
||||
deps.buildStartupBootstrapMainDeps(appLifecycleRuntimeRunner);
|
||||
const startupBootstrapRuntimeFactoryDeps: StartupBootstrapRuntimeFactoryDeps = {
|
||||
...bootstrapDeps,
|
||||
onConfigGenerated: (exitCode) => {
|
||||
setExitCode(exitCode);
|
||||
quitApp();
|
||||
},
|
||||
onGenerateConfigError: (error) => {
|
||||
logGenerateConfigError(`Failed to generate config: ${error.message}`);
|
||||
setExitCode(1);
|
||||
quitApp();
|
||||
},
|
||||
};
|
||||
|
||||
const runAndApplyStartupState = (): TStartupState => {
|
||||
const startupState = deps.runStartupBootstrapRuntime(
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
import type { TokenizerDepsRuntimeOptions } from '../../core/services/tokenizer';
|
||||
|
||||
type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
|
||||
getJlptEnabled: NonNullable<TokenizerDepsRuntimeOptions['getJlptEnabled']>;
|
||||
getCharacterDictionaryEnabled?: () => boolean;
|
||||
getNameMatchEnabled?: NonNullable<TokenizerDepsRuntimeOptions['getNameMatchEnabled']>;
|
||||
getNameMatchImagesEnabled?: NonNullable<TokenizerDepsRuntimeOptions['getNameMatchImagesEnabled']>;
|
||||
getCharacterNameImage?: NonNullable<TokenizerDepsRuntimeOptions['getCharacterNameImage']>;
|
||||
getCurrentCharacterDictionaryMediaId?: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getCurrentCharacterDictionaryMediaId']
|
||||
>;
|
||||
getCharacterNameCandidates?: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getCharacterNameCandidates']
|
||||
>;
|
||||
getFrequencyDictionaryEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
|
||||
>;
|
||||
getFrequencyDictionaryMatchMode: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryMatchMode']
|
||||
>;
|
||||
getFrequencyRank: NonNullable<TokenizerDepsRuntimeOptions['getFrequencyRank']>;
|
||||
getMinSentenceWordsForNPlusOne: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getMinSentenceWordsForNPlusOne']
|
||||
>;
|
||||
getYomitanGroupDebugEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getYomitanGroupDebugEnabled']
|
||||
>;
|
||||
recordLookup: (hit: boolean) => void;
|
||||
};
|
||||
|
||||
export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
|
||||
return (): TokenizerDepsRuntimeOptions => ({
|
||||
getYomitanExt: () => deps.getYomitanExt(),
|
||||
getYomitanSession: () => deps.getYomitanSession?.() ?? null,
|
||||
getYomitanParserWindow: () => deps.getYomitanParserWindow(),
|
||||
setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window),
|
||||
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise(),
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) =>
|
||||
deps.setYomitanParserReadyPromise(promise),
|
||||
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise(),
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) =>
|
||||
deps.setYomitanParserInitPromise(promise),
|
||||
isKnownWord: (text, reading, options) => {
|
||||
const hit = deps.isKnownWord(text, reading, options);
|
||||
deps.recordLookup(hit);
|
||||
return hit;
|
||||
},
|
||||
...(deps.getKnownWordTier
|
||||
? {
|
||||
getKnownWordTier: (text, reading, options) =>
|
||||
deps.getKnownWordTier!(text, reading, options),
|
||||
}
|
||||
: {}),
|
||||
getKnownWordMatchMode: () => deps.getKnownWordMatchMode(),
|
||||
...(deps.getKnownWordsEnabled
|
||||
? {
|
||||
getKnownWordsEnabled: () => deps.getKnownWordsEnabled!(),
|
||||
}
|
||||
: {}),
|
||||
...(deps.getNPlusOneEnabled
|
||||
? {
|
||||
getNPlusOneEnabled: () => deps.getNPlusOneEnabled!(),
|
||||
}
|
||||
: {}),
|
||||
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
|
||||
getJlptLevel: (text: string) => deps.getJlptLevel(text),
|
||||
getJlptEnabled: () => deps.getJlptEnabled(),
|
||||
...(deps.getNameMatchEnabled
|
||||
? {
|
||||
getNameMatchEnabled: () =>
|
||||
deps.getCharacterDictionaryEnabled?.() !== false && deps.getNameMatchEnabled!(),
|
||||
}
|
||||
: {}),
|
||||
...(deps.getNameMatchImagesEnabled
|
||||
? {
|
||||
getNameMatchImagesEnabled: () =>
|
||||
deps.getCharacterDictionaryEnabled?.() !== false && deps.getNameMatchImagesEnabled!(),
|
||||
}
|
||||
: {}),
|
||||
...(deps.getCharacterNameImage
|
||||
? {
|
||||
getCharacterNameImage: (term: string) => deps.getCharacterNameImage!(term),
|
||||
}
|
||||
: {}),
|
||||
...(deps.getCurrentCharacterDictionaryMediaId
|
||||
? {
|
||||
getCurrentCharacterDictionaryMediaId: () => deps.getCurrentCharacterDictionaryMediaId!(),
|
||||
}
|
||||
: {}),
|
||||
...(deps.getCharacterNameCandidates
|
||||
? {
|
||||
getCharacterNameCandidates: () => deps.getCharacterNameCandidates!(),
|
||||
}
|
||||
: {}),
|
||||
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
||||
getFrequencyDictionaryMatchMode: () => deps.getFrequencyDictionaryMatchMode(),
|
||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
|
||||
getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(),
|
||||
getMecabTokenizer: () => deps.getMecabTokenizer(),
|
||||
onTokenizationReady: (text: string) => deps.onTokenizationReady?.(text),
|
||||
});
|
||||
}
|
||||
|
||||
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>;
|
||||
showMpvOsd?: (message: string) => void;
|
||||
showLoadingOsd?: (message: string) => void;
|
||||
showLoadedOsd?: (message: string) => void;
|
||||
shouldShowOsdNotification?: () => boolean;
|
||||
setInterval?: (callback: () => void, delayMs: number) => unknown;
|
||||
clearInterval?: (timer: unknown) => void;
|
||||
}) {
|
||||
let prewarmed = false;
|
||||
let prewarmPromise: Promise<void> | null = null;
|
||||
let loadingOsdDepth = 0;
|
||||
let loadingOsdFrame = 0;
|
||||
let loadingOsdTimer: unknown = null;
|
||||
const showMpvOsd = deps.showMpvOsd;
|
||||
const showLoadingOsd = deps.showLoadingOsd ?? showMpvOsd;
|
||||
const showLoadedOsd = deps.showLoadedOsd ?? showMpvOsd;
|
||||
const setIntervalHandler =
|
||||
deps.setInterval ??
|
||||
((callback: () => void, delayMs: number): unknown => setInterval(callback, delayMs));
|
||||
const clearIntervalHandler =
|
||||
deps.clearInterval ??
|
||||
((timer: unknown): void => clearInterval(timer as ReturnType<typeof setInterval>));
|
||||
const spinnerFrames = ['|', '/', '-', '\\'];
|
||||
|
||||
const beginLoadingOsd = (): boolean => {
|
||||
if (!showLoadingOsd) {
|
||||
return false;
|
||||
}
|
||||
loadingOsdDepth += 1;
|
||||
if (loadingOsdDepth > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
loadingOsdFrame = 0;
|
||||
showLoadingOsd(`Loading subtitle annotations ${spinnerFrames[loadingOsdFrame]}`);
|
||||
loadingOsdFrame += 1;
|
||||
loadingOsdTimer = setIntervalHandler(() => {
|
||||
if (!showLoadingOsd) {
|
||||
return;
|
||||
}
|
||||
showLoadingOsd(
|
||||
`Loading subtitle annotations ${spinnerFrames[loadingOsdFrame % spinnerFrames.length]}`,
|
||||
);
|
||||
loadingOsdFrame += 1;
|
||||
}, 180);
|
||||
return true;
|
||||
};
|
||||
|
||||
const endLoadingOsd = (): void => {
|
||||
if (!showLoadedOsd) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingOsdDepth = Math.max(0, loadingOsdDepth - 1);
|
||||
if (loadingOsdDepth > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadingOsdTimer) {
|
||||
clearIntervalHandler(loadingOsdTimer);
|
||||
loadingOsdTimer = null;
|
||||
}
|
||||
showLoadedOsd('Subtitle annotations loaded');
|
||||
};
|
||||
|
||||
return async (options?: { showLoadingOsd?: boolean }): Promise<void> => {
|
||||
if (prewarmed) {
|
||||
return;
|
||||
}
|
||||
const shouldTrackLoadingOsd = options?.showLoadingOsd === true;
|
||||
const loadingOsdStarted = shouldTrackLoadingOsd ? beginLoadingOsd() : false;
|
||||
|
||||
if (!prewarmPromise) {
|
||||
prewarmPromise = (async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
deps.ensureJlptDictionaryLookup(),
|
||||
deps.ensureFrequencyDictionaryLookup(),
|
||||
]);
|
||||
prewarmed = true;
|
||||
} finally {
|
||||
prewarmPromise = null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
try {
|
||||
await prewarmPromise;
|
||||
} finally {
|
||||
if (loadingOsdStarted) {
|
||||
endLoadingOsd();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+9
-58
@@ -1,10 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createBuildTokenizerDepsMainHandler,
|
||||
createCreateMecabTokenizerAndCheckMainHandler,
|
||||
createPrewarmSubtitleDictionariesMainHandler,
|
||||
} from './subtitle-tokenization-main-deps';
|
||||
createMecabTokenizerAndCheckHandler,
|
||||
createPrewarmSubtitleDictionariesHandler,
|
||||
} from './subtitle-tokenization-warmup';
|
||||
|
||||
function createDeferred(): {
|
||||
promise: Promise<void>;
|
||||
@@ -17,59 +16,11 @@ function createDeferred(): {
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
type TokenizerMainDeps = Parameters<typeof createBuildTokenizerDepsMainHandler>[0];
|
||||
|
||||
function buildTokenizerDeps(overrides: Partial<TokenizerMainDeps> = {}) {
|
||||
return createBuildTokenizerDepsMainHandler({
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => undefined,
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => undefined,
|
||||
getYomitanParserInitPromise: () => null,
|
||||
setYomitanParserInitPromise: () => undefined,
|
||||
isKnownWord: () => false,
|
||||
recordLookup: () => undefined,
|
||||
getKnownWordMatchMode: () => 'surface',
|
||||
getNPlusOneEnabled: () => true,
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => 'N2',
|
||||
getJlptEnabled: () => true,
|
||||
getNameMatchEnabled: () => true,
|
||||
getNameMatchImagesEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyDictionaryMatchMode: () => 'surface',
|
||||
getFrequencyRank: () => 5,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => null,
|
||||
...overrides,
|
||||
})();
|
||||
}
|
||||
|
||||
test('tokenizer deps builder records every known-word lookup result', () => {
|
||||
const lookups: boolean[] = [];
|
||||
const deps = buildTokenizerDeps({
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: (hit) => lookups.push(hit),
|
||||
});
|
||||
|
||||
assert.equal(deps.isKnownWord('known'), true);
|
||||
assert.equal(deps.isKnownWord('unknown'), false);
|
||||
assert.deepEqual(lookups, [true, false]);
|
||||
});
|
||||
|
||||
test('tokenizer deps builder disables name matching when character dictionary runtime is disabled', () => {
|
||||
const deps = buildTokenizerDeps({ getCharacterDictionaryEnabled: () => false });
|
||||
|
||||
assert.equal(deps.getNameMatchEnabled?.(), false);
|
||||
assert.equal(deps.getNameMatchImagesEnabled?.(), false);
|
||||
});
|
||||
|
||||
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>({
|
||||
const run = createMecabTokenizerAndCheckHandler<Tokenizer>({
|
||||
getMecabTokenizer: () => tokenizer,
|
||||
setMecabTokenizer: (next) => {
|
||||
tokenizer = next;
|
||||
@@ -91,7 +42,7 @@ test('mecab tokenizer check creates tokenizer once and runs availability check',
|
||||
|
||||
test('dictionary prewarm runs both dictionary loaders', async () => {
|
||||
const calls: string[] = [];
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
const prewarm = createPrewarmSubtitleDictionariesHandler({
|
||||
ensureJlptDictionaryLookup: async () => {
|
||||
calls.push('jlpt');
|
||||
},
|
||||
@@ -111,7 +62,7 @@ test('dictionary prewarm shows OSD spinner while loading and completion when loa
|
||||
const jlptDeferred = createDeferred();
|
||||
const freqDeferred = createDeferred();
|
||||
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
const prewarm = createPrewarmSubtitleDictionariesHandler({
|
||||
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
|
||||
ensureFrequencyDictionaryLookup: async () => freqDeferred.promise,
|
||||
shouldShowOsdNotification: () => true,
|
||||
@@ -157,7 +108,7 @@ test('dictionary prewarm can show OSD while awaiting background-started load', a
|
||||
const jlptDeferred = createDeferred();
|
||||
const freqDeferred = createDeferred();
|
||||
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
const prewarm = createPrewarmSubtitleDictionariesHandler({
|
||||
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
|
||||
ensureFrequencyDictionaryLookup: async () => freqDeferred.promise,
|
||||
shouldShowOsdNotification: () => true,
|
||||
@@ -183,7 +134,7 @@ test('dictionary prewarm can show OSD while awaiting background-started load', a
|
||||
test('dictionary prewarm shows OSD when loading indicator is requested even if notification predicate is disabled', async () => {
|
||||
const osdMessages: string[] = [];
|
||||
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
const prewarm = createPrewarmSubtitleDictionariesHandler({
|
||||
ensureJlptDictionaryLookup: async () => undefined,
|
||||
ensureFrequencyDictionaryLookup: async () => undefined,
|
||||
shouldShowOsdNotification: () => false,
|
||||
@@ -203,7 +154,7 @@ test('dictionary prewarm clears loading OSD timer even if notifications are disa
|
||||
const freqDeferred = createDeferred();
|
||||
let shouldShowNotification = true;
|
||||
|
||||
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||
const prewarm = createPrewarmSubtitleDictionariesHandler({
|
||||
ensureJlptDictionaryLookup: async () => jlptDeferred.promise,
|
||||
ensureFrequencyDictionaryLookup: async () => freqDeferred.promise,
|
||||
shouldShowOsdNotification: () => shouldShowNotification,
|
||||
@@ -0,0 +1,115 @@
|
||||
// Creates the MeCab tokenizer on first use, then runs its availability check.
|
||||
export function createMecabTokenizerAndCheckHandler<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);
|
||||
};
|
||||
}
|
||||
|
||||
// Loads the JLPT and frequency dictionaries once (concurrent callers share the in-flight load),
|
||||
// optionally showing a spinner OSD while loading.
|
||||
export function createPrewarmSubtitleDictionariesHandler(deps: {
|
||||
ensureJlptDictionaryLookup: () => Promise<void>;
|
||||
ensureFrequencyDictionaryLookup: () => Promise<void>;
|
||||
showMpvOsd?: (message: string) => void;
|
||||
showLoadingOsd?: (message: string) => void;
|
||||
showLoadedOsd?: (message: string) => void;
|
||||
shouldShowOsdNotification?: () => boolean;
|
||||
setInterval?: (callback: () => void, delayMs: number) => unknown;
|
||||
clearInterval?: (timer: unknown) => void;
|
||||
}) {
|
||||
let prewarmed = false;
|
||||
let prewarmPromise: Promise<void> | null = null;
|
||||
let loadingOsdDepth = 0;
|
||||
let loadingOsdFrame = 0;
|
||||
let loadingOsdTimer: unknown = null;
|
||||
const showMpvOsd = deps.showMpvOsd;
|
||||
const showLoadingOsd = deps.showLoadingOsd ?? showMpvOsd;
|
||||
const showLoadedOsd = deps.showLoadedOsd ?? showMpvOsd;
|
||||
const setIntervalHandler =
|
||||
deps.setInterval ??
|
||||
((callback: () => void, delayMs: number): unknown => setInterval(callback, delayMs));
|
||||
const clearIntervalHandler =
|
||||
deps.clearInterval ??
|
||||
((timer: unknown): void => clearInterval(timer as ReturnType<typeof setInterval>));
|
||||
const spinnerFrames = ['|', '/', '-', '\\'];
|
||||
|
||||
const beginLoadingOsd = (): boolean => {
|
||||
if (!showLoadingOsd) {
|
||||
return false;
|
||||
}
|
||||
loadingOsdDepth += 1;
|
||||
if (loadingOsdDepth > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
loadingOsdFrame = 0;
|
||||
showLoadingOsd(`Loading subtitle annotations ${spinnerFrames[loadingOsdFrame]}`);
|
||||
loadingOsdFrame += 1;
|
||||
loadingOsdTimer = setIntervalHandler(() => {
|
||||
if (!showLoadingOsd) {
|
||||
return;
|
||||
}
|
||||
showLoadingOsd(
|
||||
`Loading subtitle annotations ${spinnerFrames[loadingOsdFrame % spinnerFrames.length]}`,
|
||||
);
|
||||
loadingOsdFrame += 1;
|
||||
}, 180);
|
||||
return true;
|
||||
};
|
||||
|
||||
const endLoadingOsd = (): void => {
|
||||
if (!showLoadedOsd) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingOsdDepth = Math.max(0, loadingOsdDepth - 1);
|
||||
if (loadingOsdDepth > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadingOsdTimer) {
|
||||
clearIntervalHandler(loadingOsdTimer);
|
||||
loadingOsdTimer = null;
|
||||
}
|
||||
showLoadedOsd('Subtitle annotations loaded');
|
||||
};
|
||||
|
||||
return async (options?: { showLoadingOsd?: boolean }): Promise<void> => {
|
||||
if (prewarmed) {
|
||||
return;
|
||||
}
|
||||
const shouldTrackLoadingOsd = options?.showLoadingOsd === true;
|
||||
const loadingOsdStarted = shouldTrackLoadingOsd ? beginLoadingOsd() : false;
|
||||
|
||||
if (!prewarmPromise) {
|
||||
prewarmPromise = (async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
deps.ensureJlptDictionaryLookup(),
|
||||
deps.ensureFrequencyDictionaryLookup(),
|
||||
]);
|
||||
prewarmed = true;
|
||||
} finally {
|
||||
prewarmPromise = null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
try {
|
||||
await prewarmPromise;
|
||||
} finally {
|
||||
if (loadingOsdStarted) {
|
||||
endLoadingOsd();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createDestroyTrayHandler, createEnsureTrayHandler } from './tray-lifecycle';
|
||||
import {
|
||||
createDestroyTrayHandler,
|
||||
createEnsureOverlayVisibleFromTrayClickHandler,
|
||||
createEnsureTrayHandler,
|
||||
} from './tray-lifecycle';
|
||||
|
||||
type EnsureTrayDeps = Parameters<typeof createEnsureTrayHandler>[0];
|
||||
type TrayIcon = ReturnType<EnsureTrayDeps['createEmptyImage']>;
|
||||
@@ -154,3 +158,18 @@ test('destroy tray handler destroys active tray and clears ref', () => {
|
||||
assert.equal(activeTray.destroyed, true);
|
||||
assert.equal(tray, null);
|
||||
});
|
||||
|
||||
for (const initialized of [false, true]) {
|
||||
test(`tray click shows overlay${initialized ? '' : ' after booting runtime'}`, () => {
|
||||
const calls: string[] = [];
|
||||
createEnsureOverlayVisibleFromTrayClickHandler({
|
||||
isOverlayRuntimeInitialized: () => initialized,
|
||||
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),
|
||||
})();
|
||||
assert.deepEqual(
|
||||
calls,
|
||||
initialized ? ['set-visible:true'] : ['init-overlay', 'set-visible:true'],
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -82,3 +82,17 @@ export function createDestroyTrayHandler(deps: {
|
||||
deps.setTray(null);
|
||||
};
|
||||
}
|
||||
|
||||
// Tray icon click: boot the overlay runtime on first use, then show the visible overlay.
|
||||
export function createEnsureOverlayVisibleFromTrayClickHandler(deps: {
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
initializeOverlayRuntime: () => void;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
if (!deps.isOverlayRuntimeInitialized()) {
|
||||
deps.initializeOverlayRuntime();
|
||||
}
|
||||
deps.setVisibleOverlayVisible(true);
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createDestroyTrayHandler, createEnsureTrayHandler } from './tray-lifecycle';
|
||||
import {
|
||||
createBuildDestroyTrayMainDepsHandler,
|
||||
createBuildEnsureTrayMainDepsHandler,
|
||||
} from './app-runtime-main-deps';
|
||||
createDestroyTrayHandler,
|
||||
createEnsureOverlayVisibleFromTrayClickHandler,
|
||||
createEnsureTrayHandler,
|
||||
} from './tray-lifecycle';
|
||||
import {
|
||||
createBuildTrayMenuTemplateHandler,
|
||||
createResolveTrayIconPathHandler,
|
||||
@@ -12,34 +12,41 @@ type ResolveTrayIconPathMainDeps = Parameters<typeof createResolveTrayIconPathHa
|
||||
type BuildTrayMenuTemplateMainDeps<TMenuItem> = Parameters<
|
||||
typeof createBuildTrayMenuTemplateHandler<TMenuItem>
|
||||
>[0];
|
||||
type EnsureTrayMainDeps<TTrayMenu> = Parameters<
|
||||
typeof createBuildEnsureTrayMainDepsHandler<TrayLike, TTrayMenu, TrayIconLike>
|
||||
>[0];
|
||||
type TrayLike = NonNullable<ReturnType<Parameters<typeof createEnsureTrayHandler>[0]['getTray']>>;
|
||||
type TrayIconLike = Parameters<Parameters<typeof createEnsureTrayHandler>[0]['createTray']>[0];
|
||||
type DestroyTrayMainDeps = Parameters<typeof createBuildDestroyTrayMainDepsHandler<TrayLike>>[0];
|
||||
type EnsureTrayDeps = Parameters<typeof createEnsureTrayHandler>[0];
|
||||
type DestroyTrayDeps = Parameters<typeof createDestroyTrayHandler>[0];
|
||||
|
||||
export function createTrayRuntimeHandlers<TMenuItem, TMenu>(deps: {
|
||||
resolveTrayIconPathDeps: ResolveTrayIconPathMainDeps;
|
||||
buildTrayMenuTemplateDeps: BuildTrayMenuTemplateMainDeps<TMenuItem>;
|
||||
ensureTrayDeps: Omit<EnsureTrayMainDeps<TMenu>, 'buildTrayMenu' | 'resolveTrayIconPath'>;
|
||||
destroyTrayDeps: DestroyTrayMainDeps;
|
||||
ensureTrayDeps: Omit<
|
||||
EnsureTrayDeps,
|
||||
'buildTrayMenu' | 'resolveTrayIconPath' | 'ensureOverlayVisibleFromTrayClick'
|
||||
> &
|
||||
Parameters<typeof createEnsureOverlayVisibleFromTrayClickHandler>[0];
|
||||
destroyTrayDeps: DestroyTrayDeps;
|
||||
buildMenuFromTemplate: (template: TMenuItem[]) => TMenu;
|
||||
}) {
|
||||
const resolveTrayIconPath = createResolveTrayIconPathHandler(deps.resolveTrayIconPathDeps);
|
||||
const buildTrayMenuTemplate = createBuildTrayMenuTemplateHandler(deps.buildTrayMenuTemplateDeps);
|
||||
const buildTrayMenu = () => deps.buildMenuFromTemplate(buildTrayMenuTemplate());
|
||||
|
||||
const ensureTray = createEnsureTrayHandler(
|
||||
createBuildEnsureTrayMainDepsHandler({
|
||||
...deps.ensureTrayDeps,
|
||||
buildTrayMenu: () => buildTrayMenu(),
|
||||
resolveTrayIconPath: () => resolveTrayIconPath(),
|
||||
})(),
|
||||
);
|
||||
const destroyTray = createDestroyTrayHandler(
|
||||
createBuildDestroyTrayMainDepsHandler(deps.destroyTrayDeps)(),
|
||||
);
|
||||
const {
|
||||
initializeOverlayRuntime,
|
||||
isOverlayRuntimeInitialized,
|
||||
setVisibleOverlayVisible,
|
||||
...ensureTrayDeps
|
||||
} = deps.ensureTrayDeps;
|
||||
const ensureTray = createEnsureTrayHandler({
|
||||
...ensureTrayDeps,
|
||||
buildTrayMenu: () => buildTrayMenu(),
|
||||
resolveTrayIconPath: () => resolveTrayIconPath(),
|
||||
ensureOverlayVisibleFromTrayClick: createEnsureOverlayVisibleFromTrayClickHandler({
|
||||
initializeOverlayRuntime,
|
||||
isOverlayRuntimeInitialized,
|
||||
setVisibleOverlayVisible,
|
||||
}),
|
||||
});
|
||||
const destroyTray = createDestroyTrayHandler(deps.destroyTrayDeps);
|
||||
|
||||
return {
|
||||
resolveTrayIconPath,
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { createBuildOpenYomitanSettingsMainDepsHandler } from './app-runtime-main-deps';
|
||||
import { createOpenYomitanSettingsHandler } from './yomitan-settings-opener';
|
||||
|
||||
type OpenYomitanSettingsMainDeps = Parameters<
|
||||
typeof createBuildOpenYomitanSettingsMainDepsHandler
|
||||
>[0];
|
||||
|
||||
export function createYomitanSettingsRuntime(deps: OpenYomitanSettingsMainDeps) {
|
||||
const openYomitanSettingsMainDeps = createBuildOpenYomitanSettingsMainDepsHandler(deps)();
|
||||
const openYomitanSettings = createOpenYomitanSettingsHandler(openYomitanSettingsMainDeps);
|
||||
|
||||
export function createYomitanSettingsRuntime(
|
||||
deps: Parameters<typeof createOpenYomitanSettingsHandler>[0],
|
||||
) {
|
||||
return {
|
||||
openYomitanSettings,
|
||||
openYomitanSettings: createOpenYomitanSettingsHandler(deps),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user