mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor(main): eliminate unsafe runtime cast escapes
Tighten main/runtime dependency contracts to remove non-test `as never` and `as unknown as` usage so type drift surfaces during compile/test checks instead of at runtime.
This commit is contained in:
93
src/main.ts
93
src/main.ts
@@ -514,7 +514,7 @@ let yomitanLoadInFlight: Promise<Extension | null> | null = null;
|
||||
|
||||
const buildApplyJellyfinMpvDefaultsMainDepsHandler =
|
||||
createBuildApplyJellyfinMpvDefaultsMainDepsHandler({
|
||||
sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client as never, command),
|
||||
sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client, command),
|
||||
jellyfinLangPref: JELLYFIN_LANG_PREF,
|
||||
});
|
||||
const applyJellyfinMpvDefaultsMainDeps = buildApplyJellyfinMpvDefaultsMainDepsHandler();
|
||||
@@ -522,7 +522,9 @@ const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler(
|
||||
applyJellyfinMpvDefaultsMainDeps,
|
||||
);
|
||||
|
||||
function applyJellyfinMpvDefaults(client: MpvIpcClient): void {
|
||||
function applyJellyfinMpvDefaults(
|
||||
client: Parameters<typeof applyJellyfinMpvDefaultsHandler>[0],
|
||||
): void {
|
||||
applyJellyfinMpvDefaultsHandler(client);
|
||||
}
|
||||
|
||||
@@ -718,36 +720,35 @@ const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsH
|
||||
let appTray: Tray | null = null;
|
||||
const buildSubtitleProcessingControllerMainDepsHandler =
|
||||
createBuildSubtitleProcessingControllerMainDepsHandler({
|
||||
tokenizeSubtitle: async (text: string) => {
|
||||
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
|
||||
return null;
|
||||
}
|
||||
return await tokenizeSubtitle(text);
|
||||
},
|
||||
emitSubtitle: (payload) => {
|
||||
const previousSubtitleText = appState.currentSubtitleData?.text ?? null;
|
||||
const nextSubtitleText = payload?.text ?? null;
|
||||
const subtitleChanged = previousSubtitleText !== nextSubtitleText;
|
||||
appState.currentSubtitleData = payload;
|
||||
if (subtitleChanged) {
|
||||
appState.hoveredSubtitleTokenIndex = null;
|
||||
appState.hoveredSubtitleRevision += 1;
|
||||
applyHoveredTokenOverlay();
|
||||
}
|
||||
broadcastToOverlayWindows('subtitle:set', payload);
|
||||
subtitleWsService.broadcast(payload, {
|
||||
enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX,
|
||||
mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode,
|
||||
});
|
||||
},
|
||||
logDebug: (message) => {
|
||||
logger.debug(`[subtitle-processing] ${message}`);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
});
|
||||
const subtitleProcessingControllerMainDeps =
|
||||
buildSubtitleProcessingControllerMainDepsHandler();
|
||||
tokenizeSubtitle: async (text: string) => {
|
||||
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
|
||||
return null;
|
||||
}
|
||||
return await tokenizeSubtitle(text);
|
||||
},
|
||||
emitSubtitle: (payload) => {
|
||||
const previousSubtitleText = appState.currentSubtitleData?.text ?? null;
|
||||
const nextSubtitleText = payload?.text ?? null;
|
||||
const subtitleChanged = previousSubtitleText !== nextSubtitleText;
|
||||
appState.currentSubtitleData = payload;
|
||||
if (subtitleChanged) {
|
||||
appState.hoveredSubtitleTokenIndex = null;
|
||||
appState.hoveredSubtitleRevision += 1;
|
||||
applyHoveredTokenOverlay();
|
||||
}
|
||||
broadcastToOverlayWindows('subtitle:set', payload);
|
||||
subtitleWsService.broadcast(payload, {
|
||||
enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||
topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX,
|
||||
mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode,
|
||||
});
|
||||
},
|
||||
logDebug: (message) => {
|
||||
logger.debug(`[subtitle-processing] ${message}`);
|
||||
},
|
||||
now: () => Date.now(),
|
||||
});
|
||||
const subtitleProcessingControllerMainDeps = buildSubtitleProcessingControllerMainDepsHandler();
|
||||
const subtitleProcessingController = createSubtitleProcessingController(
|
||||
subtitleProcessingControllerMainDeps,
|
||||
);
|
||||
@@ -811,20 +812,20 @@ const watchConfigPathHandler = createWatchConfigPathHandler(buildWatchConfigPath
|
||||
const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler(
|
||||
{
|
||||
setKeybindings: (keybindings) => {
|
||||
appState.keybindings = keybindings as never;
|
||||
appState.keybindings = keybindings;
|
||||
},
|
||||
refreshGlobalAndOverlayShortcuts: () => {
|
||||
refreshGlobalAndOverlayShortcuts();
|
||||
},
|
||||
setSecondarySubMode: (mode) => {
|
||||
appState.secondarySubMode = mode as never;
|
||||
appState.secondarySubMode = mode;
|
||||
},
|
||||
broadcastToOverlayWindows: (channel, payload) => {
|
||||
broadcastToOverlayWindows(channel, payload);
|
||||
},
|
||||
applyAnkiRuntimeConfigPatch: (patch) => {
|
||||
if (appState.ankiIntegration) {
|
||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch as never);
|
||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -912,7 +913,7 @@ const jlptDictionaryRuntime = createJlptDictionaryRuntimeService(
|
||||
getDictionaryRoots: () => buildDictionaryRootsHandler(),
|
||||
getJlptDictionarySearchPaths,
|
||||
setJlptLevelLookup: (lookup) => {
|
||||
appState.jlptLevelLookup = lookup as never;
|
||||
appState.jlptLevelLookup = lookup;
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
})(),
|
||||
@@ -926,7 +927,7 @@ const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService(
|
||||
getFrequencyDictionarySearchPaths,
|
||||
getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath,
|
||||
setFrequencyRankLookup: (lookup) => {
|
||||
appState.frequencyRankLookup = lookup as never;
|
||||
appState.frequencyRankLookup = lookup;
|
||||
},
|
||||
logInfo: (message) => logger.info(message),
|
||||
})(),
|
||||
@@ -968,7 +969,7 @@ function setFieldGroupingResolver(
|
||||
}
|
||||
|
||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>(
|
||||
createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal, KikuFieldGroupingChoice>({
|
||||
createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal>({
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
@@ -1257,8 +1258,7 @@ const buildPlayJellyfinItemInMpvMainDepsHandler = createBuildPlayJellyfinItemInM
|
||||
subtitleStreamIndex: params.subtitleStreamIndex ?? undefined,
|
||||
},
|
||||
),
|
||||
applyJellyfinMpvDefaults: (mpvClient) =>
|
||||
applyJellyfinMpvDefaults(mpvClient as unknown as MpvIpcClient),
|
||||
applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults(mpvClient),
|
||||
sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command),
|
||||
armQuitOnDisconnect: () => {
|
||||
jellyfinPlayQuitOnDisconnectArmed = false;
|
||||
@@ -2169,10 +2169,7 @@ const {
|
||||
},
|
||||
},
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: MpvIpcClient as unknown as new (
|
||||
socketPath: string,
|
||||
options: MpvClientRuntimeServiceOptions,
|
||||
) => MpvIpcClient,
|
||||
createClient: MpvIpcClient,
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
|
||||
@@ -2434,7 +2431,7 @@ const { appendToMpvLog, flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers(
|
||||
buildShowMpvOsdMainDeps: (appendToMpvLogHandler) => ({
|
||||
appendToMpvLog: (message) => appendToMpvLogHandler(message),
|
||||
showMpvOsdRuntime: (mpvClient, text, fallbackLog) =>
|
||||
showMpvOsdRuntime(mpvClient as never, text, fallbackLog),
|
||||
showMpvOsdRuntime(mpvClient, text, fallbackLog),
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
logInfo: (line) => logger.info(line),
|
||||
}),
|
||||
@@ -2845,7 +2842,7 @@ const {
|
||||
},
|
||||
createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
|
||||
createEmptyImage: () => nativeImage.createEmpty(),
|
||||
createTray: (icon) => new Tray(icon as never),
|
||||
createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
|
||||
trayTooltip: TRAY_TOOLTIP,
|
||||
platform: process.platform,
|
||||
logWarn: (message) => logger.warn(message),
|
||||
@@ -2910,12 +2907,12 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
||||
getOverlayWindows: () => getOverlayWindows(),
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
showDesktopNotification,
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback() as never,
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||
},
|
||||
initializeOverlayRuntimeBootstrapDeps: {
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlayRuntimeCore: (options) => initializeOverlayRuntimeCore(options as never),
|
||||
initializeOverlayRuntimeCore,
|
||||
setInvisibleOverlayVisible: (visible) => {
|
||||
overlayManager.setInvisibleOverlayVisible(visible);
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export function createBuildEnsureTrayMainDepsHandler(deps: {
|
||||
getTray: () => unknown | null;
|
||||
setTray: (tray: unknown | null) => void;
|
||||
buildTrayMenu: () => unknown;
|
||||
export function createBuildEnsureTrayMainDepsHandler<TTray, TTrayMenu, TTrayIcon>(deps: {
|
||||
getTray: () => TTray | null;
|
||||
setTray: (tray: TTray | null) => void;
|
||||
buildTrayMenu: () => TTrayMenu;
|
||||
resolveTrayIconPath: () => string | null;
|
||||
createImageFromPath: (iconPath: string) => unknown;
|
||||
createEmptyImage: () => unknown;
|
||||
createTray: (icon: unknown) => unknown;
|
||||
createImageFromPath: (iconPath: string) => TTrayIcon;
|
||||
createEmptyImage: () => TTrayIcon;
|
||||
createTray: (icon: TTrayIcon) => TTray;
|
||||
trayTooltip: string;
|
||||
platform: string;
|
||||
logWarn: (message: string) => void;
|
||||
@@ -14,13 +14,13 @@ export function createBuildEnsureTrayMainDepsHandler(deps: {
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getTray: () => deps.getTray() as never,
|
||||
setTray: (tray: unknown | null) => deps.setTray(tray),
|
||||
buildTrayMenu: () => deps.buildTrayMenu() as never,
|
||||
getTray: () => deps.getTray(),
|
||||
setTray: (tray: TTray | null) => deps.setTray(tray),
|
||||
buildTrayMenu: () => deps.buildTrayMenu(),
|
||||
resolveTrayIconPath: () => deps.resolveTrayIconPath(),
|
||||
createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath) as never,
|
||||
createEmptyImage: () => deps.createEmptyImage() as never,
|
||||
createTray: (icon: unknown) => deps.createTray(icon) as never,
|
||||
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),
|
||||
@@ -33,28 +33,28 @@ export function createBuildEnsureTrayMainDepsHandler(deps: {
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildDestroyTrayMainDepsHandler(deps: {
|
||||
getTray: () => unknown | null;
|
||||
setTray: (tray: unknown | null) => void;
|
||||
export function createBuildDestroyTrayMainDepsHandler<TTray>(deps: {
|
||||
getTray: () => TTray | null;
|
||||
setTray: (tray: TTray | null) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
getTray: () => deps.getTray() as never,
|
||||
setTray: (tray: unknown | null) => deps.setTray(tray),
|
||||
getTray: () => deps.getTray(),
|
||||
setTray: (tray: TTray | null) => deps.setTray(tray),
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps: {
|
||||
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOptions>(deps: {
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
initializeOverlayRuntimeCore: (options: unknown) => { invisibleOverlayVisible: boolean };
|
||||
buildOptions: () => unknown;
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => { invisibleOverlayVisible: boolean };
|
||||
buildOptions: () => TOptions;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) => void;
|
||||
startBackgroundWarmups: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
initializeOverlayRuntimeCore: (options: unknown) => deps.initializeOverlayRuntimeCore(options),
|
||||
buildOptions: () => deps.buildOptions() as never,
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options),
|
||||
buildOptions: () => deps.buildOptions(),
|
||||
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) =>
|
||||
deps.setOverlayRuntimeInitialized(initialized),
|
||||
@@ -62,27 +62,27 @@ export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildOpenYomitanSettingsMainDepsHandler(deps: {
|
||||
ensureYomitanExtensionLoaded: () => Promise<unknown | null>;
|
||||
export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWindow>(deps: {
|
||||
ensureYomitanExtensionLoaded: () => Promise<TYomitanExt | null>;
|
||||
openYomitanSettingsWindow: (params: {
|
||||
yomitanExt: unknown;
|
||||
getExistingWindow: () => unknown | null;
|
||||
setWindow: (window: unknown | null) => void;
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
}) => void;
|
||||
getExistingWindow: () => unknown | null;
|
||||
setWindow: (window: unknown | null) => void;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, error: unknown) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
|
||||
openYomitanSettingsWindow: (params: {
|
||||
yomitanExt: unknown;
|
||||
getExistingWindow: () => unknown | null;
|
||||
setWindow: (window: unknown | null) => void;
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
}) => deps.openYomitanSettingsWindow(params),
|
||||
getExistingWindow: () => deps.getExistingWindow(),
|
||||
setWindow: (window: unknown | null) => deps.setWindow(window),
|
||||
setWindow: (window: TWindow | null) => deps.setWindow(window),
|
||||
logWarn: (message: string) => deps.logWarn(message),
|
||||
logError: (message: string, error: unknown) => deps.logError(message, error),
|
||||
});
|
||||
|
||||
@@ -6,14 +6,14 @@ test('cli command context factory composes main deps and context handlers', () =
|
||||
const calls: string[] = [];
|
||||
const appState = {
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
mpvClient: null as unknown,
|
||||
mpvClient: null,
|
||||
texthookerPort: 5174,
|
||||
overlayRuntimeInitialized: false,
|
||||
};
|
||||
|
||||
const createContext = createCliCommandContextFactory({
|
||||
appState,
|
||||
texthookerService: { start: () => null },
|
||||
texthookerService: { isRunning: () => false, start: () => null },
|
||||
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }),
|
||||
openExternal: async () => {},
|
||||
logBrowserOpenError: () => {},
|
||||
@@ -32,11 +32,28 @@ test('cli command context factory composes main deps and context handlers', () =
|
||||
triggerFieldGrouping: async () => {},
|
||||
triggerSubsyncFromConfig: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
getAnilistStatus: () => ({ status: 'ok' }),
|
||||
getAnilistStatus: () => ({
|
||||
tokenStatus: 'resolved',
|
||||
tokenSource: 'literal',
|
||||
tokenMessage: null,
|
||||
tokenResolvedAt: null,
|
||||
tokenErrorAt: null,
|
||||
queuePending: 0,
|
||||
queueReady: 0,
|
||||
queueDeadLetter: 0,
|
||||
queueLastAttemptAt: null,
|
||||
queueLastError: null,
|
||||
}),
|
||||
clearAnilistToken: () => {},
|
||||
openAnilistSetupWindow: () => {},
|
||||
openJellyfinSetupWindow: () => {},
|
||||
getAnilistQueueStatus: () => ({ queued: 0 }),
|
||||
getAnilistQueueStatus: () => ({
|
||||
pending: 0,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
lastAttemptAt: null,
|
||||
lastError: null,
|
||||
}),
|
||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
||||
runJellyfinCommand: async () => {},
|
||||
openYomitanSettings: () => {},
|
||||
|
||||
@@ -6,14 +6,14 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
||||
const calls: string[] = [];
|
||||
const appState = {
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
mpvClient: null as unknown,
|
||||
mpvClient: null,
|
||||
texthookerPort: 5174,
|
||||
overlayRuntimeInitialized: false,
|
||||
};
|
||||
|
||||
const build = createBuildCliCommandContextMainDepsHandler({
|
||||
appState,
|
||||
texthookerService: { start: () => null },
|
||||
texthookerService: { isRunning: () => false, start: () => null },
|
||||
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }),
|
||||
openExternal: async (url) => {
|
||||
calls.push(`open:${url}`);
|
||||
@@ -49,11 +49,28 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
||||
calls.push('mark-audio');
|
||||
},
|
||||
|
||||
getAnilistStatus: () => ({ status: 'ok' }),
|
||||
getAnilistStatus: () => ({
|
||||
tokenStatus: 'resolved',
|
||||
tokenSource: 'literal',
|
||||
tokenMessage: null,
|
||||
tokenResolvedAt: null,
|
||||
tokenErrorAt: null,
|
||||
queuePending: 0,
|
||||
queueReady: 0,
|
||||
queueDeadLetter: 0,
|
||||
queueLastAttemptAt: null,
|
||||
queueLastError: null,
|
||||
}),
|
||||
clearAnilistToken: () => calls.push('clear-token'),
|
||||
openAnilistSetupWindow: () => calls.push('open-anilist-setup'),
|
||||
openJellyfinSetupWindow: () => calls.push('open-jellyfin-setup'),
|
||||
getAnilistQueueStatus: () => ({ queued: 1 }),
|
||||
getAnilistQueueStatus: () => ({
|
||||
pending: 1,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
lastAttemptAt: null,
|
||||
lastError: null,
|
||||
}),
|
||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
||||
runJellyfinCommand: async () => {
|
||||
calls.push('run-jellyfin');
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { CliArgs } from '../../cli/args';
|
||||
import type { CliCommandContextFactoryDeps } from './cli-command-context';
|
||||
|
||||
type CliCommandContextMainState = {
|
||||
mpvSocketPath: string;
|
||||
mpvClient: ReturnType<CliCommandContextFactoryDeps['getMpvClient']>;
|
||||
texthookerPort: number;
|
||||
overlayRuntimeInitialized: boolean;
|
||||
};
|
||||
|
||||
export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
appState: {
|
||||
mpvSocketPath: string;
|
||||
mpvClient: unknown | null;
|
||||
texthookerPort: number;
|
||||
overlayRuntimeInitialized: boolean;
|
||||
};
|
||||
texthookerService: unknown;
|
||||
appState: CliCommandContextMainState;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getResolvedConfig: () => { texthooker?: { openBrowser?: boolean } };
|
||||
openExternal: (url: string) => Promise<unknown>;
|
||||
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||
@@ -29,12 +32,12 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
|
||||
getAnilistStatus: () => unknown;
|
||||
getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
|
||||
clearAnilistToken: () => void;
|
||||
openAnilistSetupWindow: () => void;
|
||||
openJellyfinSetupWindow: () => void;
|
||||
getAnilistQueueStatus: () => unknown;
|
||||
processNextAnilistRetryUpdate: () => Promise<{ ok: boolean; message: string }>;
|
||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||
processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
|
||||
openYomitanSettings: () => void;
|
||||
@@ -49,14 +52,14 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
return (): CliCommandContextFactoryDeps => ({
|
||||
getSocketPath: () => deps.appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
deps.appState.mpvSocketPath = socketPath;
|
||||
},
|
||||
getMpvClient: () => deps.appState.mpvClient as never,
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
showOsd: (text: string) => deps.showMpvOsd(text),
|
||||
texthookerService: deps.texthookerService as never,
|
||||
texthookerService: deps.texthookerService,
|
||||
getTexthookerPort: () => deps.appState.texthookerPort,
|
||||
setTexthookerPort: (port: number) => {
|
||||
deps.appState.texthookerPort = port;
|
||||
@@ -80,11 +83,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
triggerFieldGrouping: () => deps.triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => deps.markLastCardAsAudioCard(),
|
||||
getAnilistStatus: () => deps.getAnilistStatus() as never,
|
||||
getAnilistStatus: () => deps.getAnilistStatus(),
|
||||
clearAnilistToken: () => deps.clearAnilistToken(),
|
||||
openAnilistSetup: () => deps.openAnilistSetupWindow(),
|
||||
openJellyfinSetup: () => deps.openJellyfinSetupWindow(),
|
||||
getAnilistQueueStatus: () => deps.getAnilistQueueStatus() as never,
|
||||
getAnilistQueueStatus: () => deps.getAnilistQueueStatus(),
|
||||
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
|
||||
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
|
||||
openYomitanSettings: () => deps.openYomitanSettings(),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { composeAppReadyRuntime } from './app-ready-composer';
|
||||
test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => {
|
||||
const composed = composeAppReadyRuntime({
|
||||
reloadConfigMainDeps: {
|
||||
reloadConfigStrict: () => ({ config: {} as never, warnings: [] }),
|
||||
reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
|
||||
logInfo: () => {},
|
||||
logWarning: () => {},
|
||||
showDesktopNotification: () => {},
|
||||
|
||||
@@ -85,7 +85,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: FakeMpvClient,
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
||||
isAutoStartOverlayEnabled: () => true,
|
||||
setOverlayVisible: () => {},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
@@ -118,7 +118,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
setYomitanParserInitPromise: () => {},
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: () => {},
|
||||
getKnownWordMatchMode: () => 'exact',
|
||||
getKnownWordMatchMode: () => 'headword',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => null,
|
||||
getJlptEnabled: () => true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main-
|
||||
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps';
|
||||
import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service';
|
||||
import type { MpvClientRuntimeServiceOptions } from '../mpv-client-runtime-service';
|
||||
import type { Config } from '../../../types';
|
||||
import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from '../mpv-subtitle-render-metrics-main-deps';
|
||||
import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics';
|
||||
import {
|
||||
@@ -30,7 +31,7 @@ type MpvClientRuntimeServiceFactoryMainDeps<TMpvClient extends RuntimeMpvClient>
|
||||
Parameters<
|
||||
typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
unknown,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>
|
||||
>[0],
|
||||
@@ -107,7 +108,7 @@ export function composeMpvRuntimeHandlers<
|
||||
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
||||
createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
unknown,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>({
|
||||
...options.mpvClientRuntimeServiceFactoryMainDeps,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type { FrequencyDictionaryLookup, JlptLevel } from '../../types';
|
||||
|
||||
type JlptLookup = (term: string) => JlptLevel | null;
|
||||
|
||||
export function createBuildDictionaryRootsMainHandler(deps: {
|
||||
dirname: string;
|
||||
appPath: string;
|
||||
@@ -8,20 +12,19 @@ export function createBuildDictionaryRootsMainHandler(deps: {
|
||||
cwd: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
}) {
|
||||
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,
|
||||
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'),
|
||||
deps.cwd,
|
||||
];
|
||||
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,
|
||||
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'),
|
||||
deps.cwd,
|
||||
];
|
||||
}
|
||||
|
||||
export function createBuildFrequencyDictionaryRootsMainHandler(deps: {
|
||||
@@ -57,7 +60,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
|
||||
isJlptEnabled: () => boolean;
|
||||
getDictionaryRoots: () => string[];
|
||||
getJlptDictionarySearchPaths: (deps: { getDictionaryRoots: () => string[] }) => string[];
|
||||
setJlptLevelLookup: (lookup: unknown) => void;
|
||||
setJlptLevelLookup: (lookup: JlptLookup) => void;
|
||||
logInfo: (message: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
@@ -66,7 +69,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
|
||||
deps.getJlptDictionarySearchPaths({
|
||||
getDictionaryRoots: () => deps.getDictionaryRoots(),
|
||||
}),
|
||||
setJlptLevelLookup: (lookup: unknown) => deps.setJlptLevelLookup(lookup),
|
||||
setJlptLevelLookup: (lookup: JlptLookup) => deps.setJlptLevelLookup(lookup),
|
||||
log: (message: string) => deps.logInfo(`[JLPT] ${message}`),
|
||||
});
|
||||
}
|
||||
@@ -79,17 +82,19 @@ export function createBuildFrequencyDictionaryRuntimeMainDepsHandler(deps: {
|
||||
getSourcePath: () => string | undefined;
|
||||
}) => string[];
|
||||
getSourcePath: () => string | undefined;
|
||||
setFrequencyRankLookup: (lookup: unknown) => void;
|
||||
setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) => void;
|
||||
logInfo: (message: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
isFrequencyDictionaryEnabled: () => deps.isFrequencyDictionaryEnabled(),
|
||||
getSearchPaths: () =>
|
||||
deps.getFrequencyDictionarySearchPaths({
|
||||
getDictionaryRoots: () => deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot),
|
||||
getDictionaryRoots: () =>
|
||||
deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot),
|
||||
getSourcePath: () => deps.getSourcePath(),
|
||||
}),
|
||||
setFrequencyRankLookup: (lookup: unknown) => deps.setFrequencyRankLookup(lookup),
|
||||
setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) =>
|
||||
deps.setFrequencyRankLookup(lookup),
|
||||
log: (message: string) => deps.logInfo(`[Frequency] ${message}`),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ test('field grouping overlay main deps builder maps window visibility and resolv
|
||||
const resolver = (choice: unknown) => calls.push(`resolver:${choice}`);
|
||||
|
||||
const deps = createBuildFieldGroupingOverlayMainDepsHandler({
|
||||
getMainWindow: () => ({ id: 'main' }),
|
||||
getMainWindow: () => ({
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
send: () => {},
|
||||
},
|
||||
}),
|
||||
getVisibleOverlayVisible: () => true,
|
||||
getInvisibleOverlayVisible: () => false,
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
|
||||
@@ -24,7 +29,7 @@ test('field grouping overlay main deps builder maps window visibility and resolv
|
||||
},
|
||||
})();
|
||||
|
||||
assert.deepEqual(deps.getMainWindow(), { id: 'main' });
|
||||
assert.equal(deps.getMainWindow()?.isDestroyed(), false);
|
||||
assert.equal(deps.getVisibleOverlayVisible(), true);
|
||||
assert.equal(deps.getInvisibleOverlayVisible(), false);
|
||||
assert.equal(deps.getResolver(), resolver);
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
export function createBuildFieldGroupingOverlayMainDepsHandler<
|
||||
TModal extends string,
|
||||
TChoice,
|
||||
>(deps: {
|
||||
getMainWindow: () => unknown | null;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
getResolver: () => ((choice: TChoice) => void) | null;
|
||||
setResolver: (resolver: ((choice: TChoice) => void) | null) => void;
|
||||
getRestoreVisibleOverlayOnModalClose: () => Set<TModal>;
|
||||
import type { FieldGroupingOverlayRuntimeOptions } from '../../core/services/field-grouping-overlay';
|
||||
|
||||
type FieldGroupingOverlayMainDeps<TModal extends string> = Omit<
|
||||
FieldGroupingOverlayRuntimeOptions<TModal>,
|
||||
'sendToVisibleOverlay'
|
||||
> & {
|
||||
sendToActiveOverlayWindow: (
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
runtimeOptions?: { restoreOnModalClose?: TModal },
|
||||
) => boolean;
|
||||
}) {
|
||||
return () => ({
|
||||
getMainWindow: () => deps.getMainWindow() as never,
|
||||
};
|
||||
|
||||
type BuiltFieldGroupingOverlayMainDeps<TModal extends string> =
|
||||
FieldGroupingOverlayRuntimeOptions<TModal> & {
|
||||
sendToVisibleOverlay: NonNullable<
|
||||
FieldGroupingOverlayRuntimeOptions<TModal>['sendToVisibleOverlay']
|
||||
>;
|
||||
};
|
||||
|
||||
export function createBuildFieldGroupingOverlayMainDepsHandler<TModal extends string>(
|
||||
deps: FieldGroupingOverlayMainDeps<TModal>,
|
||||
) {
|
||||
return (): BuiltFieldGroupingOverlayMainDeps<TModal> => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||
getResolver: () => deps.getResolver() as never,
|
||||
setResolver: (resolver: ((choice: TChoice) => void) | null) => deps.setResolver(resolver),
|
||||
getResolver: () => deps.getResolver(),
|
||||
setResolver: (resolver) => deps.setResolver(resolver),
|
||||
getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(),
|
||||
sendToVisibleOverlay: (
|
||||
channel: string,
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import type { JellyfinStoredSession } from '../../core/services/jellyfin-token-store';
|
||||
import type { ResolvedConfig } from '../../types';
|
||||
|
||||
type ResolvedJellyfinConfig = ResolvedConfig['jellyfin'];
|
||||
type ResolvedJellyfinConfigWithSession = ResolvedJellyfinConfig & {
|
||||
accessToken?: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||
getResolvedConfig: () => { jellyfin: unknown };
|
||||
loadStoredSession: () => { accessToken: string; userId: string } | null | undefined;
|
||||
getResolvedConfig: () => { jellyfin: ResolvedJellyfinConfig };
|
||||
loadStoredSession: () => JellyfinStoredSession | null | undefined;
|
||||
getEnv: (name: string) => string | undefined;
|
||||
}) {
|
||||
return () => {
|
||||
const jellyfin = deps.getResolvedConfig().jellyfin as {
|
||||
userId?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
return (): ResolvedJellyfinConfigWithSession => {
|
||||
const jellyfin = deps.getResolvedConfig().jellyfin;
|
||||
|
||||
const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? '';
|
||||
const envUserId = deps.getEnv('SUBMINER_JELLYFIN_USER_ID')?.trim() ?? '';
|
||||
@@ -20,7 +26,7 @@ export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||
...jellyfin,
|
||||
accessToken: envToken,
|
||||
userId: envUserId || storedUserId || '',
|
||||
} as never;
|
||||
};
|
||||
}
|
||||
|
||||
if (storedToken.length > 0 && storedUserId.length > 0) {
|
||||
@@ -28,24 +34,20 @@ export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||
...jellyfin,
|
||||
accessToken: storedToken,
|
||||
userId: storedUserId,
|
||||
} as never;
|
||||
};
|
||||
}
|
||||
|
||||
return jellyfin as never;
|
||||
return jellyfin;
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetJellyfinClientInfoHandler(deps: {
|
||||
getResolvedJellyfinConfig: () => {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
getDefaultJellyfinConfig: () => {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
deviceId?: string;
|
||||
};
|
||||
getResolvedJellyfinConfig: () => Partial<
|
||||
Pick<ResolvedJellyfinConfig, 'clientName' | 'clientVersion' | 'deviceId'>
|
||||
>;
|
||||
getDefaultJellyfinConfig: () => Partial<
|
||||
Pick<ResolvedJellyfinConfig, 'clientName' | 'clientVersion' | 'deviceId'>
|
||||
>;
|
||||
}) {
|
||||
return (
|
||||
config = deps.getResolvedJellyfinConfig(),
|
||||
|
||||
@@ -6,12 +6,14 @@ test('play jellyfin item in mpv main deps builder maps callbacks', async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildPlayJellyfinItemInMpvMainDepsHandler({
|
||||
ensureMpvConnectedForPlayback: async () => true,
|
||||
getMpvClient: () => ({ connected: true }),
|
||||
getMpvClient: () => ({ connected: true, send: () => {} }),
|
||||
resolvePlaybackPlan: async () => ({
|
||||
url: 'u',
|
||||
mode: 'direct',
|
||||
title: 't',
|
||||
startTimeTicks: 0,
|
||||
audioStreamIndex: null,
|
||||
subtitleStreamIndex: null,
|
||||
}),
|
||||
applyJellyfinMpvDefaults: () => calls.push('defaults'),
|
||||
sendMpvCommand: (command) => calls.push(`cmd:${command[0]}`),
|
||||
@@ -28,18 +30,57 @@ test('play jellyfin item in mpv main deps builder maps callbacks', async () => {
|
||||
assert.equal(await deps.ensureMpvConnectedForPlayback(), true);
|
||||
assert.equal(typeof deps.getMpvClient(), 'object');
|
||||
assert.deepEqual(
|
||||
await deps.resolvePlaybackPlan({ session: {} as never, clientInfo: {} as never, jellyfinConfig: {}, itemId: 'i' }),
|
||||
{ url: 'u', mode: 'direct', title: 't', startTimeTicks: 0 },
|
||||
await deps.resolvePlaybackPlan({
|
||||
session: {
|
||||
serverUrl: 'http://localhost:8096',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
username: 'alice',
|
||||
},
|
||||
clientInfo: {
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0.0',
|
||||
deviceId: 'did',
|
||||
},
|
||||
jellyfinConfig: {},
|
||||
itemId: 'i',
|
||||
}),
|
||||
{
|
||||
url: 'u',
|
||||
mode: 'direct',
|
||||
title: 't',
|
||||
startTimeTicks: 0,
|
||||
audioStreamIndex: null,
|
||||
subtitleStreamIndex: null,
|
||||
},
|
||||
);
|
||||
deps.applyJellyfinMpvDefaults({});
|
||||
deps.applyJellyfinMpvDefaults({ connected: true, send: () => {} });
|
||||
deps.sendMpvCommand(['show-text', 'x']);
|
||||
deps.armQuitOnDisconnect();
|
||||
deps.schedule(() => {}, 500);
|
||||
assert.equal(deps.convertTicksToSeconds(20_000_000), 2);
|
||||
deps.preloadExternalSubtitles({ session: {} as never, clientInfo: {} as never, itemId: 'i' });
|
||||
deps.preloadExternalSubtitles({
|
||||
session: {
|
||||
serverUrl: 'http://localhost:8096',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
username: 'alice',
|
||||
},
|
||||
clientInfo: {
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0.0',
|
||||
deviceId: 'did',
|
||||
},
|
||||
itemId: 'i',
|
||||
});
|
||||
deps.setActivePlayback({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay' });
|
||||
deps.setLastProgressAtMs(0);
|
||||
deps.reportPlaying({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay', eventName: 'start' });
|
||||
deps.reportPlaying({
|
||||
itemId: 'i',
|
||||
mediaSourceId: undefined,
|
||||
playMethod: 'DirectPlay',
|
||||
eventName: 'start',
|
||||
});
|
||||
deps.showMpvOsd('ok');
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
|
||||
@@ -54,7 +54,7 @@ test('playback handler drives mpv commands and playback state', async () => {
|
||||
const reportPayloads: Array<Record<string, unknown>> = [];
|
||||
const handler = createPlayJellyfinItemInMpvHandler({
|
||||
ensureMpvConnectedForPlayback: async () => true,
|
||||
getMpvClient: () => ({ connected: true }),
|
||||
getMpvClient: () => ({ connected: true, send: () => {} }),
|
||||
resolvePlaybackPlan: async () => ({
|
||||
url: 'https://stream.example/video.m3u8',
|
||||
mode: 'direct',
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
type JellyfinSession = {
|
||||
serverUrl: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
};
|
||||
import type { JellyfinAuthSession, JellyfinPlaybackPlan } from '../../core/services/jellyfin';
|
||||
import type { JellyfinConfig } from '../../types';
|
||||
import type { MpvRuntimeClientLike } from '../../core/services/mpv';
|
||||
|
||||
type JellyfinClientInfo = {
|
||||
clientName: string;
|
||||
@@ -11,15 +8,6 @@ type JellyfinClientInfo = {
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
type JellyfinPlaybackPlan = {
|
||||
url: string;
|
||||
mode: 'direct' | 'transcode';
|
||||
title: string;
|
||||
startTimeTicks: number;
|
||||
audioStreamIndex?: number | null;
|
||||
subtitleStreamIndex?: number | null;
|
||||
};
|
||||
|
||||
type ActivePlaybackState = {
|
||||
itemId: string;
|
||||
mediaSourceId: undefined;
|
||||
@@ -28,26 +16,24 @@ type ActivePlaybackState = {
|
||||
playMethod: 'DirectPlay' | 'Transcode';
|
||||
};
|
||||
|
||||
type MpvClientLike = unknown;
|
||||
|
||||
export function createPlayJellyfinItemInMpvHandler(deps: {
|
||||
ensureMpvConnectedForPlayback: () => Promise<boolean>;
|
||||
getMpvClient: () => MpvClientLike | null;
|
||||
getMpvClient: () => MpvRuntimeClientLike | null;
|
||||
resolvePlaybackPlan: (params: {
|
||||
session: JellyfinSession;
|
||||
session: JellyfinAuthSession;
|
||||
clientInfo: JellyfinClientInfo;
|
||||
jellyfinConfig: unknown;
|
||||
jellyfinConfig: JellyfinConfig;
|
||||
itemId: string;
|
||||
audioStreamIndex?: number | null;
|
||||
subtitleStreamIndex?: number | null;
|
||||
}) => Promise<JellyfinPlaybackPlan>;
|
||||
applyJellyfinMpvDefaults: (mpvClient: MpvClientLike) => void;
|
||||
applyJellyfinMpvDefaults: (mpvClient: MpvRuntimeClientLike) => void;
|
||||
sendMpvCommand: (command: Array<string | number>) => void;
|
||||
armQuitOnDisconnect: () => void;
|
||||
schedule: (callback: () => void, delayMs: number) => void;
|
||||
convertTicksToSeconds: (ticks: number) => number;
|
||||
preloadExternalSubtitles: (params: {
|
||||
session: JellyfinSession;
|
||||
session: JellyfinAuthSession;
|
||||
clientInfo: JellyfinClientInfo;
|
||||
itemId: string;
|
||||
}) => void;
|
||||
@@ -64,9 +50,9 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
|
||||
showMpvOsd: (text: string) => void;
|
||||
}) {
|
||||
return async (params: {
|
||||
session: JellyfinSession;
|
||||
session: JellyfinAuthSession;
|
||||
clientInfo: JellyfinClientInfo;
|
||||
jellyfinConfig: unknown;
|
||||
jellyfinConfig: JellyfinConfig;
|
||||
itemId: string;
|
||||
audioStreamIndex?: number | null;
|
||||
subtitleStreamIndex?: number | null;
|
||||
@@ -96,7 +82,11 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
|
||||
if (params.setQuitOnDisconnectArm !== false) {
|
||||
deps.armQuitOnDisconnect();
|
||||
}
|
||||
deps.sendMpvCommand(['set_property', 'force-media-title', `[Jellyfin/${plan.mode}] ${plan.title}`]);
|
||||
deps.sendMpvCommand([
|
||||
'set_property',
|
||||
'force-media-title',
|
||||
`[Jellyfin/${plan.mode}] ${plan.title}`,
|
||||
]);
|
||||
deps.sendMpvCommand(['set_property', 'sid', 'no']);
|
||||
deps.schedule(() => {
|
||||
deps.sendMpvCommand(['set_property', 'sid', 'no']);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Config } from '../../types';
|
||||
|
||||
export type MpvClientRuntimeServiceOptions = {
|
||||
getResolvedConfig: () => unknown;
|
||||
getResolvedConfig: () => Config;
|
||||
autoStartOverlay: boolean;
|
||||
setOverlayVisible: (visible: boolean) => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
|
||||
@@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults main deps builder maps callbacks', () => {
|
||||
jellyfinLangPref: 'ja,jp',
|
||||
})();
|
||||
|
||||
deps.sendMpvCommandRuntime({}, ['set_property', 'aid', 'auto']);
|
||||
deps.sendMpvCommandRuntime({ connected: true, send: () => {} }, ['set_property', 'aid', 'auto']);
|
||||
assert.equal(deps.jellyfinLangPref, 'ja,jp');
|
||||
assert.deepEqual(calls, ['set_property:aid:auto']);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults sends expected property commands', () => {
|
||||
jellyfinLangPref: 'ja,jp',
|
||||
});
|
||||
|
||||
applyDefaults({});
|
||||
applyDefaults({ connected: true, send: () => {} });
|
||||
assert.deepEqual(calls, [
|
||||
'set_property:sub-auto:fuzzy',
|
||||
'set_property:aid:auto',
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
type MpvClientLike = unknown;
|
||||
import type { MpvRuntimeClientLike } from '../../core/services/mpv';
|
||||
|
||||
export function createApplyJellyfinMpvDefaultsHandler(deps: {
|
||||
sendMpvCommandRuntime: (
|
||||
client: MpvClientLike,
|
||||
command: [string, string, string],
|
||||
) => void;
|
||||
sendMpvCommandRuntime: (client: MpvRuntimeClientLike, command: [string, string, string]) => void;
|
||||
jellyfinLangPref: string;
|
||||
}) {
|
||||
return (client: MpvClientLike): void => {
|
||||
return (client: MpvRuntimeClientLike): void => {
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']);
|
||||
deps.sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']);
|
||||
@@ -18,9 +15,7 @@ export function createApplyJellyfinMpvDefaultsHandler(deps: {
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetDefaultSocketPathHandler(deps: {
|
||||
platform: string;
|
||||
}) {
|
||||
export function createGetDefaultSocketPathHandler(deps: { platform: string }) {
|
||||
return (): string => {
|
||||
if (deps.platform === 'win32') {
|
||||
return '\\\\.\\pipe\\subminer-socket';
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
createHandleMpvTimePosChangeHandler,
|
||||
} from './mpv-main-event-actions';
|
||||
|
||||
type MpvEventClient = {
|
||||
on: (...args: any[]) => unknown;
|
||||
};
|
||||
type MpvEventClient = Parameters<ReturnType<typeof createBindMpvClientEventHandlers>>[0];
|
||||
|
||||
export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
reportJellyfinRemoteStopped: () => void;
|
||||
@@ -119,7 +117,8 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
updateSubtitleRenderMetrics: (patch) => deps.updateSubtitleRenderMetrics(patch),
|
||||
});
|
||||
const handleMpvSecondarySubtitleVisibility = createHandleMpvSecondarySubtitleVisibilityHandler({
|
||||
setPreviousSecondarySubVisibility: (visible) => deps.setPreviousSecondarySubVisibility(visible),
|
||||
setPreviousSecondarySubVisibility: (visible) =>
|
||||
deps.setPreviousSecondarySubVisibility(visible),
|
||||
});
|
||||
|
||||
createBindMpvClientEventHandlers({
|
||||
@@ -134,6 +133,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
onPauseChange: handleMpvPauseChange,
|
||||
onSubtitleMetricsChange: handleMpvSubtitleMetricsChange,
|
||||
onSecondarySubtitleVisibility: handleMpvSecondarySubtitleVisibility,
|
||||
})(mpvClient as never);
|
||||
})(mpvClient);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,10 @@ test('append to mpv log main deps map filesystem functions and log path', async
|
||||
|
||||
test('show mpv osd main deps map runtime delegates and logging callback', () => {
|
||||
const calls: string[] = [];
|
||||
const client = { id: 'mpv' };
|
||||
const client = {
|
||||
connected: true,
|
||||
send: () => {},
|
||||
};
|
||||
const deps = createBuildShowMpvOsdMainDepsHandler({
|
||||
appendToMpvLog: (message) => calls.push(`append:${message}`),
|
||||
showMpvOsdRuntime: (_mpvClient, text, fallbackLog) => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export function createBuildAppendToMpvLogMainDepsHandler(deps: {
|
||||
logPath: string;
|
||||
dirname: (targetPath: string) => string;
|
||||
mkdir: (targetPath: string, options: { recursive: boolean }) => Promise<void>;
|
||||
appendFile: (targetPath: string, data: string, options: { encoding: 'utf8' }) => Promise<void>;
|
||||
now: () => Date;
|
||||
}) {
|
||||
return () => ({
|
||||
import type { createAppendToMpvLogHandler, createShowMpvOsdHandler } from './mpv-osd-log';
|
||||
|
||||
type AppendToMpvLogMainDeps = Parameters<typeof createAppendToMpvLogHandler>[0];
|
||||
type ShowMpvOsdMainDeps = Parameters<typeof createShowMpvOsdHandler>[0];
|
||||
|
||||
export function createBuildAppendToMpvLogMainDepsHandler(deps: AppendToMpvLogMainDeps) {
|
||||
return (): AppendToMpvLogMainDeps => ({
|
||||
logPath: deps.logPath,
|
||||
dirname: (targetPath: string) => deps.dirname(targetPath),
|
||||
mkdir: (targetPath: string, options: { recursive: boolean }) => deps.mkdir(targetPath, options),
|
||||
@@ -15,24 +14,12 @@ export function createBuildAppendToMpvLogMainDepsHandler(deps: {
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildShowMpvOsdMainDepsHandler(deps: {
|
||||
appendToMpvLog: (message: string) => void;
|
||||
showMpvOsdRuntime: (
|
||||
mpvClient: unknown | null,
|
||||
text: string,
|
||||
fallbackLog: (line: string) => void,
|
||||
) => void;
|
||||
getMpvClient: () => unknown | null;
|
||||
logInfo: (line: string) => void;
|
||||
}) {
|
||||
return () => ({
|
||||
export function createBuildShowMpvOsdMainDepsHandler(deps: ShowMpvOsdMainDeps) {
|
||||
return (): ShowMpvOsdMainDeps => ({
|
||||
appendToMpvLog: (message: string) => deps.appendToMpvLog(message),
|
||||
showMpvOsdRuntime: (
|
||||
mpvClient: unknown | null,
|
||||
text: string,
|
||||
fallbackLog: (line: string) => void,
|
||||
) => deps.showMpvOsdRuntime(mpvClient, text, fallbackLog),
|
||||
getMpvClient: () => deps.getMpvClient() as never,
|
||||
showMpvOsdRuntime: (mpvClient, text, fallbackLog) =>
|
||||
deps.showMpvOsdRuntime(mpvClient, text, fallbackLog),
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
logInfo: (line: string) => deps.logInfo(line),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
import type { KikuFieldGroupingChoice } from '../../types';
|
||||
import { createOverlayRuntimeBootstrapHandlers } from './overlay-runtime-bootstrap-handlers';
|
||||
|
||||
test('overlay runtime bootstrap handlers compose options builder and bootstrap handler', () => {
|
||||
const appState = {
|
||||
backendOverride: null as string | null,
|
||||
windowTracker: null as unknown,
|
||||
windowTracker: null as BaseWindowTracker | null,
|
||||
subtitleTimingTracker: null as unknown,
|
||||
mpvClient: null as unknown,
|
||||
mpvClient: null,
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
runtimeOptionsManager: null as unknown,
|
||||
runtimeOptionsManager: null,
|
||||
ankiIntegration: null as unknown,
|
||||
};
|
||||
let initialized = false;
|
||||
@@ -39,7 +41,13 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h
|
||||
getOverlayWindows: () => [],
|
||||
getResolvedConfig: () => ({}),
|
||||
showDesktopNotification: () => {},
|
||||
createFieldGroupingCallback: () => (async () => 'combined' as never),
|
||||
createFieldGroupingCallback: () => async () =>
|
||||
({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
cancelled: true,
|
||||
}) as KikuFieldGroupingChoice,
|
||||
getKnownWordCacheStatePath: () => '/tmp/known.json',
|
||||
},
|
||||
initializeOverlayRuntimeBootstrapDeps: {
|
||||
|
||||
@@ -6,17 +6,24 @@ import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-ru
|
||||
type InitializeOverlayRuntimeMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeMainDepsHandler
|
||||
>[0];
|
||||
type InitializeOverlayRuntimeOptions = ReturnType<
|
||||
ReturnType<typeof createBuildInitializeOverlayRuntimeOptionsHandler>
|
||||
>;
|
||||
type InitializeOverlayRuntimeBootstrapMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler
|
||||
typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<InitializeOverlayRuntimeOptions>
|
||||
>[0];
|
||||
|
||||
export function createOverlayRuntimeBootstrapHandlers(deps: {
|
||||
initializeOverlayRuntimeMainDeps: InitializeOverlayRuntimeMainDeps;
|
||||
initializeOverlayRuntimeBootstrapDeps: Omit<InitializeOverlayRuntimeBootstrapMainDeps, 'buildOptions'>;
|
||||
initializeOverlayRuntimeBootstrapDeps: Omit<
|
||||
InitializeOverlayRuntimeBootstrapMainDeps,
|
||||
'buildOptions'
|
||||
>;
|
||||
}) {
|
||||
const buildInitializeOverlayRuntimeOptionsHandler = createBuildInitializeOverlayRuntimeOptionsHandler(
|
||||
createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(),
|
||||
);
|
||||
const buildInitializeOverlayRuntimeOptionsHandler =
|
||||
createBuildInitializeOverlayRuntimeOptionsHandler(
|
||||
createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(),
|
||||
);
|
||||
const initializeOverlayRuntime = createInitializeOverlayRuntimeHandler(
|
||||
createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler({
|
||||
...deps.initializeOverlayRuntimeBootstrapDeps,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-runtime-options-main-deps';
|
||||
|
||||
test('overlay runtime main deps builder maps runtime state and callbacks', () => {
|
||||
const calls: string[] = [];
|
||||
const appState = {
|
||||
backendOverride: 'x11' as string | null,
|
||||
windowTracker: null as unknown,
|
||||
windowTracker: null as BaseWindowTracker | null,
|
||||
subtitleTimingTracker: { id: 'tracker' } as unknown,
|
||||
mpvClient: null as { send?: (payload: { command: string[] }) => void } | null,
|
||||
mpvSocketPath: '/tmp/mpv.sock',
|
||||
@@ -36,7 +37,12 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
getOverlayWindows: () => [],
|
||||
getResolvedConfig: () => ({}),
|
||||
showDesktopNotification: () => calls.push('notify'),
|
||||
createFieldGroupingCallback: () => async () => ({ cancelled: true }),
|
||||
createFieldGroupingCallback: () => async () => ({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
cancelled: true,
|
||||
}),
|
||||
getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json',
|
||||
});
|
||||
|
||||
@@ -58,7 +64,11 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
deps.syncOverlayShortcuts();
|
||||
deps.showDesktopNotification('title', {});
|
||||
|
||||
deps.setWindowTracker({ id: 'tracker' });
|
||||
const tracker = {
|
||||
close: () => {},
|
||||
getWindowGeometry: () => null,
|
||||
} as unknown as BaseWindowTracker;
|
||||
deps.setWindowTracker(tracker);
|
||||
deps.setAnkiIntegration({ id: 'anki' });
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
@@ -72,6 +82,6 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
|
||||
'sync-shortcuts',
|
||||
'notify',
|
||||
]);
|
||||
assert.deepEqual(appState.windowTracker, { id: 'tracker' });
|
||||
assert.equal(appState.windowTracker, tracker);
|
||||
assert.deepEqual(appState.ankiIntegration, { id: 'anki' });
|
||||
});
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { AnkiConnectConfig } from '../../types';
|
||||
import type { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options';
|
||||
|
||||
type OverlayRuntimeOptionsMainDeps = Parameters<
|
||||
typeof createBuildInitializeOverlayRuntimeOptionsHandler
|
||||
>[0];
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
appState: {
|
||||
backendOverride: string | null;
|
||||
windowTracker: unknown | null;
|
||||
subtitleTimingTracker: unknown | null;
|
||||
mpvClient: unknown | null;
|
||||
windowTracker: Parameters<OverlayRuntimeOptionsMainDeps['setWindowTracker']>[0];
|
||||
subtitleTimingTracker: ReturnType<OverlayRuntimeOptionsMainDeps['getSubtitleTimingTracker']>;
|
||||
mpvClient: ReturnType<OverlayRuntimeOptionsMainDeps['getMpvClient']>;
|
||||
mpvSocketPath: string;
|
||||
runtimeOptionsManager: unknown | null;
|
||||
ankiIntegration: unknown | null;
|
||||
runtimeOptionsManager: ReturnType<OverlayRuntimeOptionsMainDeps['getRuntimeOptionsManager']>;
|
||||
ankiIntegration: Parameters<OverlayRuntimeOptionsMainDeps['setAnkiIntegration']>[0];
|
||||
};
|
||||
overlayManager: {
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
@@ -25,27 +30,36 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
createMainWindow: () => void;
|
||||
createInvisibleWindow: () => void;
|
||||
registerGlobalShortcuts: () => void;
|
||||
updateVisibleOverlayBounds: (geometry: { x: number; y: number; width: number; height: number }) => void;
|
||||
updateVisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => void;
|
||||
updateInvisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => void;
|
||||
getOverlayWindows: () => unknown[];
|
||||
getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows'];
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
createFieldGroupingCallback: () => unknown;
|
||||
createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback'];
|
||||
getKnownWordCacheStatePath: () => string;
|
||||
}) {
|
||||
return () => ({
|
||||
return (): OverlayRuntimeOptionsMainDeps => ({
|
||||
getBackendOverride: () => deps.appState.backendOverride,
|
||||
getInitialInvisibleOverlayVisibility: () => deps.getInitialInvisibleOverlayVisibility(),
|
||||
createMainWindow: () => deps.createMainWindow(),
|
||||
createInvisibleWindow: () => deps.createInvisibleWindow(),
|
||||
registerGlobalShortcuts: () => deps.registerGlobalShortcuts(),
|
||||
updateVisibleOverlayBounds: (geometry: { x: number; y: number; width: number; height: number }) =>
|
||||
deps.updateVisibleOverlayBounds(geometry),
|
||||
updateVisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => deps.updateVisibleOverlayBounds(geometry),
|
||||
updateInvisibleOverlayBounds: (geometry: {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -54,28 +68,25 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
|
||||
}) => deps.updateInvisibleOverlayBounds(geometry),
|
||||
isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(),
|
||||
isInvisibleOverlayVisible: () => deps.overlayManager.getInvisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () => deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
updateVisibleOverlayVisibility: () =>
|
||||
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () =>
|
||||
deps.overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||
getOverlayWindows: () => deps.getOverlayWindows() as never,
|
||||
getOverlayWindows: () => deps.getOverlayWindows(),
|
||||
syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||
setWindowTracker: (tracker: unknown | null) => {
|
||||
setWindowTracker: (tracker) => {
|
||||
deps.appState.windowTracker = tracker;
|
||||
},
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
getSubtitleTimingTracker: () => deps.appState.subtitleTimingTracker,
|
||||
getMpvClient: () =>
|
||||
(deps.appState.mpvClient as { send?: (payload: { command: string[] }) => void } | null),
|
||||
getMpvClient: () => deps.appState.mpvClient,
|
||||
getMpvSocketPath: () => deps.appState.mpvSocketPath,
|
||||
getRuntimeOptionsManager: () =>
|
||||
deps.appState.runtimeOptionsManager as
|
||||
| { getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig }
|
||||
| null,
|
||||
setAnkiIntegration: (integration: unknown | null) => {
|
||||
getRuntimeOptionsManager: () => deps.appState.runtimeOptionsManager,
|
||||
setAnkiIntegration: (integration) => {
|
||||
deps.appState.ankiIntegration = integration;
|
||||
},
|
||||
showDesktopNotification: deps.showDesktopNotification,
|
||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback() as never,
|
||||
createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
WindowGeometry,
|
||||
} from '../../types';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
|
||||
type OverlayRuntimeOptions = {
|
||||
backendOverride: string | null;
|
||||
@@ -20,7 +21,7 @@ type OverlayRuntimeOptions = {
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
syncOverlayShortcuts: () => void;
|
||||
setWindowTracker: (tracker: unknown | null) => void;
|
||||
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
getSubtitleTimingTracker: () => unknown | null;
|
||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||
@@ -50,7 +51,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
syncOverlayShortcuts: () => void;
|
||||
setWindowTracker: (tracker: unknown | null) => void;
|
||||
setWindowTracker: (tracker: BaseWindowTracker | null) => void;
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
getSubtitleTimingTracker: () => unknown | null;
|
||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { BaseWindowTracker } from '../../window-trackers';
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './overlay-visibility-runtime-main-deps';
|
||||
@@ -7,13 +9,14 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
|
||||
let trackerNotReadyWarningShown = false;
|
||||
const mainWindow = { id: 'main' } as never;
|
||||
const invisibleWindow = { id: 'invisible' } as never;
|
||||
const tracker = { id: 'tracker' } as unknown as BaseWindowTracker;
|
||||
|
||||
const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({
|
||||
getMainWindow: () => mainWindow,
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
getVisibleOverlayVisible: () => true,
|
||||
getInvisibleOverlayVisible: () => false,
|
||||
getWindowTracker: () => ({ id: 'tracker' }),
|
||||
getWindowTracker: () => tracker,
|
||||
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
|
||||
setTrackerNotReadyWarningShown: (shown) => {
|
||||
trackerNotReadyWarningShown = shown;
|
||||
|
||||
@@ -2,29 +2,19 @@ import type { BrowserWindow } from 'electron';
|
||||
import type { WindowGeometry } from '../../types';
|
||||
import type { OverlayVisibilityRuntimeDeps } from '../overlay-visibility-runtime';
|
||||
|
||||
export function createBuildOverlayVisibilityRuntimeMainDepsHandler(deps: {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
getWindowTracker: () => unknown | null;
|
||||
getTrackerNotReadyWarningShown: () => boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||
enforceOverlayLayerOrder: () => void;
|
||||
syncOverlayShortcuts: () => void;
|
||||
}) {
|
||||
export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
|
||||
deps: OverlayVisibilityRuntimeDeps,
|
||||
) {
|
||||
return (): OverlayVisibilityRuntimeDeps => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getInvisibleWindow: () => deps.getInvisibleWindow(),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
|
||||
getWindowTracker: () => deps.getWindowTracker() as never,
|
||||
getWindowTracker: () => deps.getWindowTracker(),
|
||||
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.updateVisibleOverlayBounds(geometry),
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.updateInvisibleOverlayBounds(geometry),
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
test('reload config main deps builder maps callbacks and fail handlers', async () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildReloadConfigMainDepsHandler({
|
||||
reloadConfigStrict: () => ({ ok: true }),
|
||||
reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
|
||||
logInfo: (message) => calls.push(`info:${message}`),
|
||||
logWarning: (message) => calls.push(`warn:${message}`),
|
||||
showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`),
|
||||
@@ -24,7 +24,11 @@ test('reload config main deps builder maps callbacks and fail handlers', async (
|
||||
},
|
||||
})();
|
||||
|
||||
assert.deepEqual(deps.reloadConfigStrict(), { ok: true });
|
||||
assert.deepEqual(deps.reloadConfigStrict(), {
|
||||
ok: true,
|
||||
path: '/tmp/config.jsonc',
|
||||
warnings: [],
|
||||
});
|
||||
deps.logInfo('x');
|
||||
deps.logWarning('y');
|
||||
deps.showDesktopNotification('SubMiner', { body: 'warn' });
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
export function createBuildReloadConfigMainDepsHandler(deps: {
|
||||
reloadConfigStrict: () => unknown;
|
||||
logInfo: (message: string) => void;
|
||||
logWarning: (message: string) => void;
|
||||
showDesktopNotification: (title: string, options: { body: string }) => void;
|
||||
startConfigHotReload: () => void;
|
||||
refreshAnilistClientSecretState: (options: { force: boolean }) => Promise<unknown>;
|
||||
failHandlers: {
|
||||
logError: (details: string) => void;
|
||||
showErrorBox: (title: string, details: string) => void;
|
||||
quit: () => void;
|
||||
};
|
||||
}) {
|
||||
return () => ({
|
||||
reloadConfigStrict: () => deps.reloadConfigStrict() as never,
|
||||
import type { createCriticalConfigErrorHandler, createReloadConfigHandler } from './startup-config';
|
||||
|
||||
type ReloadConfigMainDeps = Parameters<typeof createReloadConfigHandler>[0];
|
||||
type CriticalConfigErrorMainDeps = Parameters<typeof createCriticalConfigErrorHandler>[0];
|
||||
|
||||
export function createBuildReloadConfigMainDepsHandler(deps: ReloadConfigMainDeps) {
|
||||
return (): ReloadConfigMainDeps => ({
|
||||
reloadConfigStrict: () => deps.reloadConfigStrict(),
|
||||
logInfo: (message: string) => deps.logInfo(message),
|
||||
logWarning: (message: string) => deps.logWarning(message),
|
||||
showDesktopNotification: (title: string, options: { body: string }) =>
|
||||
@@ -22,25 +15,20 @@ export function createBuildReloadConfigMainDepsHandler(deps: {
|
||||
deps.refreshAnilistClientSecretState(options),
|
||||
failHandlers: {
|
||||
logError: (details: string) => deps.failHandlers.logError(details),
|
||||
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||
showErrorBox: (title: string, details: string) =>
|
||||
deps.failHandlers.showErrorBox(title, details),
|
||||
quit: () => deps.failHandlers.quit(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildCriticalConfigErrorMainDepsHandler(deps: {
|
||||
getConfigPath: () => string;
|
||||
failHandlers: {
|
||||
logError: (details: string) => void;
|
||||
showErrorBox: (title: string, details: string) => void;
|
||||
quit: () => void;
|
||||
};
|
||||
}) {
|
||||
return () => ({
|
||||
export function createBuildCriticalConfigErrorMainDepsHandler(deps: CriticalConfigErrorMainDeps) {
|
||||
return (): CriticalConfigErrorMainDeps => ({
|
||||
getConfigPath: () => deps.getConfigPath(),
|
||||
failHandlers: {
|
||||
logError: (details: string) => deps.failHandlers.logError(details),
|
||||
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||
showErrorBox: (title: string, details: string) =>
|
||||
deps.failHandlers.showErrorBox(title, details),
|
||||
quit: () => deps.failHandlers.quit(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
test('tokenizer deps builder records known-word lookups and maps readers', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildTokenizerDepsMainHandler({
|
||||
getYomitanExt: () => ({ id: 'ext' }),
|
||||
getYomitanParserWindow: () => ({ id: 'window' }),
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => calls.push('set-window'),
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => calls.push('set-ready'),
|
||||
@@ -18,22 +18,22 @@ test('tokenizer deps builder records known-word lookups and maps readers', () =>
|
||||
setYomitanParserInitPromise: () => calls.push('set-init'),
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: (hit) => calls.push(`lookup:${hit}`),
|
||||
getKnownWordMatchMode: () => 'exact',
|
||||
getKnownWordMatchMode: () => 'surface',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => 'N2',
|
||||
getJlptEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyRank: () => 5,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
getMecabTokenizer: () => null,
|
||||
})();
|
||||
|
||||
assert.equal(deps.isKnownWord('known'), true);
|
||||
assert.equal(deps.isKnownWord('unknown'), false);
|
||||
deps.setYomitanParserWindow({});
|
||||
deps.setYomitanParserWindow(null);
|
||||
deps.setYomitanParserReadyPromise(null);
|
||||
deps.setYomitanParserInitPromise(null);
|
||||
assert.equal(deps.getMinSentenceWordsForNPlusOne(), 3);
|
||||
assert.equal(deps.getMinSentenceWordsForNPlusOne?.(), 3);
|
||||
assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
export function createBuildTokenizerDepsMainHandler(deps: {
|
||||
getYomitanExt: () => unknown;
|
||||
getYomitanParserWindow: () => unknown;
|
||||
setYomitanParserWindow: (window: unknown) => void;
|
||||
getYomitanParserReadyPromise: () => Promise<void> | null;
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||
getYomitanParserInitPromise: () => Promise<boolean> | null;
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||
isKnownWord: (text: string) => boolean;
|
||||
import type { TokenizerDepsRuntimeOptions } from '../../core/services/tokenizer';
|
||||
|
||||
type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
|
||||
getJlptEnabled: NonNullable<TokenizerDepsRuntimeOptions['getJlptEnabled']>;
|
||||
getFrequencyDictionaryEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
|
||||
>;
|
||||
getFrequencyRank: NonNullable<TokenizerDepsRuntimeOptions['getFrequencyRank']>;
|
||||
getMinSentenceWordsForNPlusOne: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getMinSentenceWordsForNPlusOne']
|
||||
>;
|
||||
getYomitanGroupDebugEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getYomitanGroupDebugEnabled']
|
||||
>;
|
||||
recordLookup: (hit: boolean) => void;
|
||||
getKnownWordMatchMode: () => unknown;
|
||||
getMinSentenceWordsForNPlusOne: () => number;
|
||||
getJlptLevel: (text: string) => unknown;
|
||||
getJlptEnabled: () => boolean;
|
||||
getFrequencyDictionaryEnabled: () => boolean;
|
||||
getFrequencyRank: (text: string) => unknown;
|
||||
getYomitanGroupDebugEnabled: () => boolean;
|
||||
getMecabTokenizer: () => unknown;
|
||||
}) {
|
||||
return () => ({
|
||||
getYomitanExt: () => deps.getYomitanExt() as never,
|
||||
getYomitanParserWindow: () => deps.getYomitanParserWindow() as never,
|
||||
setYomitanParserWindow: (window: unknown) => deps.setYomitanParserWindow(window),
|
||||
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise() as never,
|
||||
};
|
||||
|
||||
export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
|
||||
return (): TokenizerDepsRuntimeOptions => ({
|
||||
getYomitanExt: () => deps.getYomitanExt(),
|
||||
getYomitanParserWindow: () => deps.getYomitanParserWindow(),
|
||||
setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window),
|
||||
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise(),
|
||||
setYomitanParserReadyPromise: (promise: Promise<void> | null) =>
|
||||
deps.setYomitanParserReadyPromise(promise),
|
||||
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise() as never,
|
||||
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise(),
|
||||
setYomitanParserInitPromise: (promise: Promise<boolean> | null) =>
|
||||
deps.setYomitanParserInitPromise(promise),
|
||||
isKnownWord: (text: string) => {
|
||||
@@ -32,14 +31,14 @@ export function createBuildTokenizerDepsMainHandler(deps: {
|
||||
deps.recordLookup(hit);
|
||||
return hit;
|
||||
},
|
||||
getKnownWordMatchMode: () => deps.getKnownWordMatchMode() as never,
|
||||
getKnownWordMatchMode: () => deps.getKnownWordMatchMode(),
|
||||
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
|
||||
getJlptLevel: (text: string) => deps.getJlptLevel(text) as never,
|
||||
getJlptLevel: (text: string) => deps.getJlptLevel(text),
|
||||
getJlptEnabled: () => deps.getJlptEnabled(),
|
||||
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text) as never,
|
||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
|
||||
getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(),
|
||||
getMecabTokenizer: () => deps.getMecabTokenizer() as never,
|
||||
getMecabTokenizer: () => deps.getMecabTokenizer(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { createDestroyTrayHandler, createEnsureTrayHandler } from './tray-lifecycle';
|
||||
import { createBuildDestroyTrayMainDepsHandler, createBuildEnsureTrayMainDepsHandler } from './app-runtime-main-deps';
|
||||
import { createBuildTrayMenuTemplateHandler, createResolveTrayIconPathHandler } from './tray-main-actions';
|
||||
import {
|
||||
createBuildDestroyTrayMainDepsHandler,
|
||||
createBuildEnsureTrayMainDepsHandler,
|
||||
} from './app-runtime-main-deps';
|
||||
import {
|
||||
createBuildTrayMenuTemplateHandler,
|
||||
createResolveTrayIconPathHandler,
|
||||
} from './tray-main-actions';
|
||||
import {
|
||||
createBuildResolveTrayIconPathMainDepsHandler,
|
||||
createBuildTrayMenuTemplateMainDepsHandler,
|
||||
} from './tray-main-deps';
|
||||
|
||||
type ResolveTrayIconPathMainDeps = Parameters<typeof createBuildResolveTrayIconPathMainDepsHandler>[0];
|
||||
type ResolveTrayIconPathMainDeps = Parameters<
|
||||
typeof createBuildResolveTrayIconPathMainDepsHandler
|
||||
>[0];
|
||||
type BuildTrayMenuTemplateMainDeps<TMenuItem> = Parameters<
|
||||
typeof createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>
|
||||
>[0];
|
||||
type EnsureTrayMainDeps = Parameters<typeof createBuildEnsureTrayMainDepsHandler>[0];
|
||||
type DestroyTrayMainDeps = Parameters<typeof createBuildDestroyTrayMainDepsHandler>[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];
|
||||
|
||||
export function createTrayRuntimeHandlers<TMenuItem, TMenu>(deps: {
|
||||
resolveTrayIconPathDeps: ResolveTrayIconPathMainDeps;
|
||||
buildTrayMenuTemplateDeps: BuildTrayMenuTemplateMainDeps<TMenuItem>;
|
||||
ensureTrayDeps: Omit<EnsureTrayMainDeps, 'buildTrayMenu' | 'resolveTrayIconPath'>;
|
||||
ensureTrayDeps: Omit<EnsureTrayMainDeps<TMenu>, 'buildTrayMenu' | 'resolveTrayIconPath'>;
|
||||
destroyTrayDeps: DestroyTrayMainDeps;
|
||||
buildMenuFromTemplate: (template: TMenuItem[]) => TMenu;
|
||||
}) {
|
||||
|
||||
Reference in New Issue
Block a user