mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
feat(stats): wire stats server, overlay, and CLI into main process
- Stats server auto-start on immersion tracker init - Stats overlay toggle via keybinding and IPC - Stats CLI command (subminer stats) with cleanup mode - mpv plugin menu integration for stats toggle - CLI args for --stats, --stats-cleanup, --stats-response-path
This commit is contained in:
@@ -143,6 +143,12 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => {
|
|||||||
assert.equal(dictionaryTarget.dictionary, true);
|
assert.equal(dictionaryTarget.dictionary, true);
|
||||||
assert.equal(dictionaryTarget.dictionaryTarget, '/tmp/example.mkv');
|
assert.equal(dictionaryTarget.dictionaryTarget, '/tmp/example.mkv');
|
||||||
|
|
||||||
|
const stats = parseArgs(['--stats', '--stats-response-path', '/tmp/subminer-stats-response.json']);
|
||||||
|
assert.equal(stats.stats, true);
|
||||||
|
assert.equal(stats.statsResponsePath, '/tmp/subminer-stats-response.json');
|
||||||
|
assert.equal(hasExplicitCommand(stats), true);
|
||||||
|
assert.equal(shouldStartApp(stats), true);
|
||||||
|
|
||||||
const jellyfinLibraries = parseArgs(['--jellyfin-libraries']);
|
const jellyfinLibraries = parseArgs(['--jellyfin-libraries']);
|
||||||
assert.equal(jellyfinLibraries.jellyfinLibraries, true);
|
assert.equal(jellyfinLibraries.jellyfinLibraries, true);
|
||||||
assert.equal(hasExplicitCommand(jellyfinLibraries), true);
|
assert.equal(hasExplicitCommand(jellyfinLibraries), true);
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export interface CliArgs {
|
|||||||
anilistRetryQueue: boolean;
|
anilistRetryQueue: boolean;
|
||||||
dictionary: boolean;
|
dictionary: boolean;
|
||||||
dictionaryTarget?: string;
|
dictionaryTarget?: string;
|
||||||
|
stats: boolean;
|
||||||
|
statsCleanup?: boolean;
|
||||||
|
statsCleanupVocab?: boolean;
|
||||||
|
statsResponsePath?: string;
|
||||||
jellyfin: boolean;
|
jellyfin: boolean;
|
||||||
jellyfinLogin: boolean;
|
jellyfinLogin: boolean;
|
||||||
jellyfinLogout: boolean;
|
jellyfinLogout: boolean;
|
||||||
@@ -97,6 +101,9 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
anilistSetup: false,
|
anilistSetup: false,
|
||||||
anilistRetryQueue: false,
|
anilistRetryQueue: false,
|
||||||
dictionary: false,
|
dictionary: false,
|
||||||
|
stats: false,
|
||||||
|
statsCleanup: false,
|
||||||
|
statsCleanupVocab: false,
|
||||||
jellyfin: false,
|
jellyfin: false,
|
||||||
jellyfinLogin: false,
|
jellyfinLogin: false,
|
||||||
jellyfinLogout: false,
|
jellyfinLogout: false,
|
||||||
@@ -162,6 +169,15 @@ export function parseArgs(argv: string[]): CliArgs {
|
|||||||
} else if (arg === '--dictionary-target') {
|
} else if (arg === '--dictionary-target') {
|
||||||
const value = readValue(argv[i + 1]);
|
const value = readValue(argv[i + 1]);
|
||||||
if (value) args.dictionaryTarget = value;
|
if (value) args.dictionaryTarget = value;
|
||||||
|
} else if (arg === '--stats') args.stats = true;
|
||||||
|
else if (arg === '--stats-cleanup') args.statsCleanup = true;
|
||||||
|
else if (arg === '--stats-cleanup-vocab') args.statsCleanupVocab = true;
|
||||||
|
else if (arg.startsWith('--stats-response-path=')) {
|
||||||
|
const value = arg.split('=', 2)[1];
|
||||||
|
if (value) args.statsResponsePath = value;
|
||||||
|
} else if (arg === '--stats-response-path') {
|
||||||
|
const value = readValue(argv[i + 1]);
|
||||||
|
if (value) args.statsResponsePath = value;
|
||||||
} else if (arg === '--jellyfin') args.jellyfin = true;
|
} else if (arg === '--jellyfin') args.jellyfin = true;
|
||||||
else if (arg === '--jellyfin-login') args.jellyfinLogin = true;
|
else if (arg === '--jellyfin-login') args.jellyfinLogin = true;
|
||||||
else if (arg === '--jellyfin-logout') args.jellyfinLogout = true;
|
else if (arg === '--jellyfin-logout') args.jellyfinLogout = true;
|
||||||
@@ -331,6 +347,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
|||||||
args.anilistSetup ||
|
args.anilistSetup ||
|
||||||
args.anilistRetryQueue ||
|
args.anilistRetryQueue ||
|
||||||
args.dictionary ||
|
args.dictionary ||
|
||||||
|
args.stats ||
|
||||||
args.jellyfin ||
|
args.jellyfin ||
|
||||||
args.jellyfinLogin ||
|
args.jellyfinLogin ||
|
||||||
args.jellyfinLogout ||
|
args.jellyfinLogout ||
|
||||||
@@ -367,6 +384,7 @@ export function shouldStartApp(args: CliArgs): boolean {
|
|||||||
args.markAudioCard ||
|
args.markAudioCard ||
|
||||||
args.openRuntimeOptions ||
|
args.openRuntimeOptions ||
|
||||||
args.dictionary ||
|
args.dictionary ||
|
||||||
|
args.stats ||
|
||||||
args.jellyfin ||
|
args.jellyfin ||
|
||||||
args.jellyfinPlay ||
|
args.jellyfinPlay ||
|
||||||
args.texthooker
|
args.texthooker
|
||||||
@@ -408,6 +426,7 @@ export function shouldRunSettingsOnlyStartup(args: CliArgs): boolean {
|
|||||||
!args.anilistSetup &&
|
!args.anilistSetup &&
|
||||||
!args.anilistRetryQueue &&
|
!args.anilistRetryQueue &&
|
||||||
!args.dictionary &&
|
!args.dictionary &&
|
||||||
|
!args.stats &&
|
||||||
!args.jellyfin &&
|
!args.jellyfin &&
|
||||||
!args.jellyfinLogin &&
|
!args.jellyfinLogin &&
|
||||||
!args.jellyfinLogout &&
|
!args.jellyfinLogout &&
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ test('printHelp includes configured texthooker port', () => {
|
|||||||
assert.match(output, /--help\s+Show this help/);
|
assert.match(output, /--help\s+Show this help/);
|
||||||
assert.match(output, /default: 7777/);
|
assert.match(output, /default: 7777/);
|
||||||
assert.match(output, /--launch-mpv/);
|
assert.match(output, /--launch-mpv/);
|
||||||
|
assert.match(output, /--stats\s+Open the stats dashboard in your browser/);
|
||||||
assert.match(output, /--refresh-known-words/);
|
assert.match(output, /--refresh-known-words/);
|
||||||
assert.match(output, /--setup\s+Open first-run setup window/);
|
assert.match(output, /--setup\s+Open first-run setup window/);
|
||||||
assert.match(output, /--anilist-status/);
|
assert.match(output, /--anilist-status/);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ ${B}Session${R}
|
|||||||
--start Connect to mpv and launch overlay
|
--start Connect to mpv and launch overlay
|
||||||
--launch-mpv ${D}[targets...]${R} Launch mpv with the SubMiner mpv profile and exit
|
--launch-mpv ${D}[targets...]${R} Launch mpv with the SubMiner mpv profile and exit
|
||||||
--stop Stop the running instance
|
--stop Stop the running instance
|
||||||
|
--stats Open the stats dashboard in your browser
|
||||||
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
--texthooker Start texthooker server only ${D}(no overlay)${R}
|
||||||
|
|
||||||
${B}Overlay${R}
|
${B}Overlay${R}
|
||||||
|
|||||||
140
src/main.ts
140
src/main.ts
@@ -303,6 +303,8 @@ import {
|
|||||||
upsertYomitanDictionarySettings,
|
upsertYomitanDictionarySettings,
|
||||||
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
||||||
} from './core/services';
|
} from './core/services';
|
||||||
|
import { startStatsServer } from './core/services/stats-server';
|
||||||
|
import { registerStatsOverlayToggle, destroyStatsWindow } from './core/services/stats-window.js';
|
||||||
import {
|
import {
|
||||||
createFirstRunSetupService,
|
createFirstRunSetupService,
|
||||||
shouldAutoOpenFirstRunSetup,
|
shouldAutoOpenFirstRunSetup,
|
||||||
@@ -325,11 +327,18 @@ import {
|
|||||||
} from './main/runtime/windows-mpv-shortcuts';
|
} from './main/runtime/windows-mpv-shortcuts';
|
||||||
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
|
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
|
||||||
import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps';
|
import { createBuildImmersionTrackerStartupMainDepsHandler } from './main/runtime/immersion-startup-main-deps';
|
||||||
|
import {
|
||||||
|
createRunStatsCliCommandHandler,
|
||||||
|
writeStatsCliCommandResponse,
|
||||||
|
} from './main/runtime/stats-cli-command';
|
||||||
|
import { resolveLegacyVocabularyPosFromTokens } from './core/services/immersion-tracker/legacy-vocabulary-pos';
|
||||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||||
import {
|
import {
|
||||||
guessAnilistMediaInfo,
|
guessAnilistMediaInfo,
|
||||||
updateAnilistPostWatchProgress,
|
updateAnilistPostWatchProgress,
|
||||||
} from './core/services/anilist/anilist-updater';
|
} from './core/services/anilist/anilist-updater';
|
||||||
|
import { createCoverArtFetcher } from './core/services/anilist/cover-art-fetcher';
|
||||||
|
import { createAnilistRateLimiter } from './core/services/anilist/rate-limiter';
|
||||||
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
|
import { createJellyfinTokenStore } from './core/services/jellyfin-token-store';
|
||||||
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options-ipc';
|
||||||
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
||||||
@@ -614,6 +623,11 @@ app.setPath('userData', USER_DATA_PATH);
|
|||||||
let forceQuitTimer: ReturnType<typeof setTimeout> | null = null;
|
let forceQuitTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
function requestAppQuit(): void {
|
function requestAppQuit(): void {
|
||||||
|
destroyStatsWindow();
|
||||||
|
if (appState.statsServer) {
|
||||||
|
appState.statsServer.close();
|
||||||
|
appState.statsServer = null;
|
||||||
|
}
|
||||||
if (!forceQuitTimer) {
|
if (!forceQuitTimer) {
|
||||||
forceQuitTimer = setTimeout(() => {
|
forceQuitTimer = setTimeout(() => {
|
||||||
logger.warn('App quit timed out; forcing process exit.');
|
logger.warn('App quit timed out; forcing process exit.');
|
||||||
@@ -917,6 +931,10 @@ const buildMainSubsyncRuntimeMainDepsHandler = createBuildMainSubsyncRuntimeMain
|
|||||||
const immersionMediaRuntime = createImmersionMediaRuntime(
|
const immersionMediaRuntime = createImmersionMediaRuntime(
|
||||||
buildImmersionMediaRuntimeMainDepsHandler(),
|
buildImmersionMediaRuntimeMainDepsHandler(),
|
||||||
);
|
);
|
||||||
|
const statsCoverArtFetcher = createCoverArtFetcher(
|
||||||
|
createAnilistRateLimiter(),
|
||||||
|
createLogger('main:stats-cover-art'),
|
||||||
|
);
|
||||||
const anilistStateRuntime = createAnilistStateRuntime(buildAnilistStateRuntimeMainDepsHandler());
|
const anilistStateRuntime = createAnilistStateRuntime(buildAnilistStateRuntimeMainDepsHandler());
|
||||||
const configDerivedRuntime = createConfigDerivedRuntime(buildConfigDerivedRuntimeMainDepsHandler());
|
const configDerivedRuntime = createConfigDerivedRuntime(buildConfigDerivedRuntimeMainDepsHandler());
|
||||||
const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsHandler());
|
const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsHandler());
|
||||||
@@ -1025,11 +1043,11 @@ function maybeSignalPluginAutoplayReady(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let appTray: Tray | null = null;
|
let appTray: Tray | null = null;
|
||||||
|
let tokenizeSubtitleDeferred: ((text: string) => Promise<SubtitleData>) | null = null;
|
||||||
const buildSubtitleProcessingControllerMainDepsHandler =
|
const buildSubtitleProcessingControllerMainDepsHandler =
|
||||||
createBuildSubtitleProcessingControllerMainDepsHandler({
|
createBuildSubtitleProcessingControllerMainDepsHandler({
|
||||||
tokenizeSubtitle: async (text: string) => {
|
tokenizeSubtitle: async (text: string) =>
|
||||||
return await tokenizeSubtitle(text);
|
tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : { text, tokens: null },
|
||||||
},
|
|
||||||
emitSubtitle: (payload) => {
|
emitSubtitle: (payload) => {
|
||||||
appState.currentSubtitleData = payload;
|
appState.currentSubtitleData = payload;
|
||||||
broadcastToOverlayWindows('subtitle:set', payload);
|
broadcastToOverlayWindows('subtitle:set', payload);
|
||||||
@@ -2400,16 +2418,82 @@ const {
|
|||||||
});
|
});
|
||||||
registerProtocolUrlHandlersHandler();
|
registerProtocolUrlHandlersHandler();
|
||||||
|
|
||||||
|
const statsDistPath = path.join(__dirname, '..', 'stats', 'dist');
|
||||||
|
const statsPreloadPath = path.join(__dirname, 'preload-stats.js');
|
||||||
|
|
||||||
|
const ensureStatsServerStarted = (): string => {
|
||||||
|
const tracker = appState.immersionTracker;
|
||||||
|
if (!tracker) {
|
||||||
|
throw new Error('Immersion tracker failed to initialize.');
|
||||||
|
}
|
||||||
|
if (!appState.statsServer) {
|
||||||
|
appState.statsServer = startStatsServer({
|
||||||
|
port: getResolvedConfig().stats.serverPort,
|
||||||
|
staticDir: statsDistPath,
|
||||||
|
tracker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return `http://127.0.0.1:${getResolvedConfig().stats.serverPort}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveLegacyVocabularyPos = async (row: {
|
||||||
|
headword: string;
|
||||||
|
word: string;
|
||||||
|
reading: string | null;
|
||||||
|
}) => {
|
||||||
|
const tokenizer = appState.mecabTokenizer;
|
||||||
|
if (!tokenizer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookupTexts = [...new Set([row.headword, row.word, row.reading ?? ''])]
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter((value) => value.length > 0);
|
||||||
|
|
||||||
|
for (const lookupText of lookupTexts) {
|
||||||
|
const tokens = await tokenizer.tokenize(lookupText);
|
||||||
|
const resolved = resolveLegacyVocabularyPosFromTokens(lookupText, tokens);
|
||||||
|
if (resolved) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const immersionTrackerStartupMainDeps: Parameters<
|
const immersionTrackerStartupMainDeps: Parameters<
|
||||||
typeof createBuildImmersionTrackerStartupMainDepsHandler
|
typeof createBuildImmersionTrackerStartupMainDepsHandler
|
||||||
>[0] = {
|
>[0] = {
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
||||||
createTrackerService: (params) => new ImmersionTrackerService(params),
|
createTrackerService: (params) =>
|
||||||
|
new ImmersionTrackerService({
|
||||||
|
...params,
|
||||||
|
resolveLegacyVocabularyPos,
|
||||||
|
}),
|
||||||
setTracker: (tracker) => {
|
setTracker: (tracker) => {
|
||||||
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
||||||
|
appState.immersionTracker?.setCoverArtFetcher(statsCoverArtFetcher);
|
||||||
|
if (tracker) {
|
||||||
|
// Start HTTP stats server (once)
|
||||||
|
if (!appState.statsServer) {
|
||||||
|
const config = getResolvedConfig();
|
||||||
|
if (config.stats.autoStartServer) {
|
||||||
|
ensureStatsServerStarted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register stats overlay toggle IPC handler (idempotent)
|
||||||
|
registerStatsOverlayToggle({
|
||||||
|
staticDir: statsDistPath,
|
||||||
|
preloadPath: statsPreloadPath,
|
||||||
|
getToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||||
|
resolveBounds: () => getCurrentOverlayGeometry(),
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
shouldAutoConnectMpv: () => !appState.statsStartupInProgress,
|
||||||
seedTrackerFromCurrentMedia: () => {
|
seedTrackerFromCurrentMedia: () => {
|
||||||
void immersionMediaRuntime.seedFromCurrentMedia();
|
void immersionMediaRuntime.seedFromCurrentMedia();
|
||||||
},
|
},
|
||||||
@@ -2420,6 +2504,10 @@ const immersionTrackerStartupMainDeps: Parameters<
|
|||||||
const createImmersionTrackerStartup = createImmersionTrackerStartupHandler(
|
const createImmersionTrackerStartup = createImmersionTrackerStartupHandler(
|
||||||
createBuildImmersionTrackerStartupMainDepsHandler(immersionTrackerStartupMainDeps)(),
|
createBuildImmersionTrackerStartupMainDepsHandler(immersionTrackerStartupMainDeps)(),
|
||||||
);
|
);
|
||||||
|
const recordTrackedCardsMined = (count: number, noteIds?: number[]): void => {
|
||||||
|
ensureImmersionTrackerStarted();
|
||||||
|
appState.immersionTracker?.recordCardsMined(count, noteIds);
|
||||||
|
};
|
||||||
let hasAttemptedImmersionTrackerStartup = false;
|
let hasAttemptedImmersionTrackerStartup = false;
|
||||||
const ensureImmersionTrackerStarted = (): void => {
|
const ensureImmersionTrackerStarted = (): void => {
|
||||||
if (hasAttemptedImmersionTrackerStartup || appState.immersionTracker) {
|
if (hasAttemptedImmersionTrackerStartup || appState.immersionTracker) {
|
||||||
@@ -2429,6 +2517,34 @@ const ensureImmersionTrackerStarted = (): void => {
|
|||||||
createImmersionTrackerStartup();
|
createImmersionTrackerStartup();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runStatsCliCommand = createRunStatsCliCommandHandler({
|
||||||
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
ensureImmersionTrackerStarted: () => {
|
||||||
|
appState.statsStartupInProgress = true;
|
||||||
|
try {
|
||||||
|
ensureImmersionTrackerStarted();
|
||||||
|
} finally {
|
||||||
|
appState.statsStartupInProgress = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ensureVocabularyCleanupTokenizerReady: async () => {
|
||||||
|
await createMecabTokenizerAndCheck();
|
||||||
|
},
|
||||||
|
getImmersionTracker: () => appState.immersionTracker,
|
||||||
|
ensureStatsServerStarted: () => ensureStatsServerStarted(),
|
||||||
|
openExternal: (url: string) => shell.openExternal(url),
|
||||||
|
writeResponse: (responsePath, payload) => {
|
||||||
|
writeStatsCliCommandResponse(responsePath, payload);
|
||||||
|
},
|
||||||
|
exitAppWithCode: (code) => {
|
||||||
|
process.exitCode = code;
|
||||||
|
requestAppQuit();
|
||||||
|
},
|
||||||
|
logInfo: (message) => logger.info(message),
|
||||||
|
logWarn: (message, error) => logger.warn(message, error),
|
||||||
|
logError: (message, error) => logger.error(message, error),
|
||||||
|
});
|
||||||
|
|
||||||
const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||||
reloadConfigMainDeps: {
|
reloadConfigMainDeps: {
|
||||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||||
@@ -2576,10 +2692,13 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
|||||||
setVisibleOverlayVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
setVisibleOverlayVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
handleInitialArgs: () => handleInitialArgs(),
|
handleInitialArgs: () => handleInitialArgs(),
|
||||||
|
shouldUseMinimalStartup: () =>
|
||||||
|
Boolean(appState.initialArgs?.stats && appState.initialArgs?.statsCleanup),
|
||||||
shouldSkipHeavyStartup: () =>
|
shouldSkipHeavyStartup: () =>
|
||||||
Boolean(
|
Boolean(
|
||||||
appState.initialArgs &&
|
appState.initialArgs &&
|
||||||
(shouldRunSettingsOnlyStartup(appState.initialArgs) ||
|
(shouldRunSettingsOnlyStartup(appState.initialArgs) ||
|
||||||
|
appState.initialArgs.stats ||
|
||||||
appState.initialArgs.dictionary ||
|
appState.initialArgs.dictionary ||
|
||||||
appState.initialArgs.setup),
|
appState.initialArgs.setup),
|
||||||
),
|
),
|
||||||
@@ -2728,6 +2847,8 @@ const {
|
|||||||
ensureImmersionTrackerInitialized: () => {
|
ensureImmersionTrackerInitialized: () => {
|
||||||
ensureImmersionTrackerStarted();
|
ensureImmersionTrackerStarted();
|
||||||
},
|
},
|
||||||
|
tokenizeSubtitleForImmersion: async (text): Promise<SubtitleData | null> =>
|
||||||
|
tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : null,
|
||||||
updateCurrentMediaPath: (path) => {
|
updateCurrentMediaPath: (path) => {
|
||||||
autoPlayReadySignalMediaPath = null;
|
autoPlayReadySignalMediaPath = null;
|
||||||
currentMediaTokenizationGate.updateCurrentMediaPath(path);
|
currentMediaTokenizationGate.updateCurrentMediaPath(path);
|
||||||
@@ -2939,6 +3060,7 @@ const {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
tokenizeSubtitleDeferred = tokenizeSubtitle;
|
||||||
|
|
||||||
function createMpvClientRuntimeService(): MpvIpcClient {
|
function createMpvClientRuntimeService(): MpvIpcClient {
|
||||||
return createMpvClientRuntimeServiceHandler() as MpvIpcClient;
|
return createMpvClientRuntimeServiceHandler() as MpvIpcClient;
|
||||||
@@ -3113,6 +3235,7 @@ function destroyTray(): void {
|
|||||||
|
|
||||||
function initializeOverlayRuntime(): void {
|
function initializeOverlayRuntime(): void {
|
||||||
initializeOverlayRuntimeHandler();
|
initializeOverlayRuntimeHandler();
|
||||||
|
appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
|
||||||
syncOverlayMpvSubtitleSuppression();
|
syncOverlayMpvSubtitleSuppression();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3283,9 +3406,9 @@ const buildMineSentenceCardMainDepsHandler = createBuildMineSentenceCardMainDeps
|
|||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
showMpvOsd: (text) => showMpvOsd(text),
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
mineSentenceCardCore,
|
mineSentenceCardCore,
|
||||||
recordCardsMined: (count) => {
|
recordCardsMined: (count, noteIds) => {
|
||||||
ensureImmersionTrackerStarted();
|
ensureImmersionTrackerStarted();
|
||||||
appState.immersionTracker?.recordCardsMined(count);
|
appState.immersionTracker?.recordCardsMined(count, noteIds);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const mineSentenceCardHandler = createMineSentenceCardHandler(
|
const mineSentenceCardHandler = createMineSentenceCardHandler(
|
||||||
@@ -3455,6 +3578,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||||
getKeybindings: () => appState.keybindings,
|
getKeybindings: () => appState.keybindings,
|
||||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||||
|
getStatsToggleKey: () => getResolvedConfig().stats.toggleKey,
|
||||||
getControllerConfig: () => getResolvedConfig().controller,
|
getControllerConfig: () => getResolvedConfig().controller,
|
||||||
saveControllerPreference: ({ preferredGamepadId, preferredGamepadLabel }) => {
|
saveControllerPreference: ({ preferredGamepadId, preferredGamepadLabel }) => {
|
||||||
configService.patchRawConfig({
|
configService.patchRawConfig({
|
||||||
@@ -3477,6 +3601,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
||||||
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
||||||
appendClipboardVideoToQueue: () => appendClipboardVideoToQueue(),
|
appendClipboardVideoToQueue: () => appendClipboardVideoToQueue(),
|
||||||
|
getImmersionTracker: () => appState.immersionTracker,
|
||||||
},
|
},
|
||||||
ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({
|
ankiJimakuDeps: createAnkiJimakuIpcRuntimeServiceDeps({
|
||||||
patchAnkiConnectEnabled: (enabled: boolean) => {
|
patchAnkiConnectEnabled: (enabled: boolean) => {
|
||||||
@@ -3489,6 +3614,7 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
|||||||
getAnkiIntegration: () => appState.ankiIntegration,
|
getAnkiIntegration: () => appState.ankiIntegration,
|
||||||
setAnkiIntegration: (integration: AnkiIntegration | null) => {
|
setAnkiIntegration: (integration: AnkiIntegration | null) => {
|
||||||
appState.ankiIntegration = integration;
|
appState.ankiIntegration = integration;
|
||||||
|
appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
|
||||||
},
|
},
|
||||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||||
showDesktopNotification,
|
showDesktopNotification,
|
||||||
@@ -3551,6 +3677,8 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
|
|||||||
return await characterDictionaryRuntime.generateForCurrentMedia(targetPath);
|
return await characterDictionaryRuntime.generateForCurrentMedia(targetPath);
|
||||||
},
|
},
|
||||||
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
||||||
|
runStatsCommand: (argsFromCommand: CliArgs, source: CliCommandSource) =>
|
||||||
|
runStatsCliCommand(argsFromCommand, source),
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
cycleSecondarySubMode: () => handleCycleSecondarySubMode(),
|
cycleSecondarySubMode: () => handleCycleSecondarySubMode(),
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export interface AppReadyRuntimeDepsFactoryInput {
|
|||||||
onCriticalConfigErrors?: AppReadyRuntimeDeps['onCriticalConfigErrors'];
|
onCriticalConfigErrors?: AppReadyRuntimeDeps['onCriticalConfigErrors'];
|
||||||
logDebug?: AppReadyRuntimeDeps['logDebug'];
|
logDebug?: AppReadyRuntimeDeps['logDebug'];
|
||||||
now?: AppReadyRuntimeDeps['now'];
|
now?: AppReadyRuntimeDeps['now'];
|
||||||
|
shouldUseMinimalStartup?: AppReadyRuntimeDeps['shouldUseMinimalStartup'];
|
||||||
shouldSkipHeavyStartup?: AppReadyRuntimeDeps['shouldSkipHeavyStartup'];
|
shouldSkipHeavyStartup?: AppReadyRuntimeDeps['shouldSkipHeavyStartup'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ export function createAppReadyRuntimeDeps(
|
|||||||
onCriticalConfigErrors: params.onCriticalConfigErrors,
|
onCriticalConfigErrors: params.onCriticalConfigErrors,
|
||||||
logDebug: params.logDebug,
|
logDebug: params.logDebug,
|
||||||
now: params.now,
|
now: params.now,
|
||||||
|
shouldUseMinimalStartup: params.shouldUseMinimalStartup,
|
||||||
shouldSkipHeavyStartup: params.shouldSkipHeavyStartup,
|
shouldSkipHeavyStartup: params.shouldSkipHeavyStartup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ test('generateForCurrentMedia emits structured-content glossary so image stays w
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -361,6 +362,7 @@ test('generateForCurrentMedia applies configured open states to character dictio
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -518,6 +520,7 @@ test('generateForCurrentMedia reapplies collapsible open states when using cache
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -533,6 +536,7 @@ test('generateForCurrentMedia reapplies collapsible open states when using cache
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -661,6 +665,7 @@ test('generateForCurrentMedia adds kana aliases for romanized names when native
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'Konosuba',
|
title: 'Konosuba',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -783,6 +788,7 @@ test('generateForCurrentMedia indexes kanji family and given names using AniList
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'Rascal Does Not Dream of Bunny Girl Senpai',
|
title: 'Rascal Does Not Dream of Bunny Girl Senpai',
|
||||||
|
season: null,
|
||||||
episode: 1,
|
episode: 1,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -904,6 +910,7 @@ test('generateForCurrentMedia indexes AniList alternative character names for al
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -1028,6 +1035,7 @@ test('generateForCurrentMedia skips AniList characters without a native name whe
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -1148,6 +1156,7 @@ test('generateForCurrentMedia uses AniList first and last name hints to build ka
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'Konosuba',
|
title: 'Konosuba',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -1265,6 +1274,7 @@ test('generateForCurrentMedia includes AniList gender age birthday and blood typ
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -1408,6 +1418,7 @@ test('generateForCurrentMedia preserves duplicate surface forms across different
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -1548,6 +1559,7 @@ test('getOrCreateCurrentSnapshot persists and reuses normalized snapshot data',
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -1703,6 +1715,7 @@ test('getOrCreateCurrentSnapshot rebuilds snapshots written with an older format
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -1842,6 +1855,7 @@ test('generateForCurrentMedia logs progress while resolving and rebuilding snaps
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -2014,6 +2028,7 @@ test('generateForCurrentMedia downloads shared voice actor images once per AniLi
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -2194,6 +2209,7 @@ test('buildMergedDictionary combines stored snapshots into one stable dictionary
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: current.title,
|
title: current.title,
|
||||||
|
season: null,
|
||||||
episode: current.episode,
|
episode: current.episode,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -2481,6 +2497,7 @@ test('buildMergedDictionary reapplies collapsible open states from current confi
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: current.title,
|
title: current.title,
|
||||||
|
season: null,
|
||||||
episode: current.episode,
|
episode: current.episode,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -2500,6 +2517,7 @@ test('buildMergedDictionary reapplies collapsible open states from current confi
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: current.title,
|
title: current.title,
|
||||||
|
season: null,
|
||||||
episode: current.episode,
|
episode: current.episode,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
@@ -2663,6 +2681,7 @@ test('generateForCurrentMedia paces AniList requests and character image downloa
|
|||||||
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
guessAnilistMediaInfo: async () => ({
|
guessAnilistMediaInfo: async () => ({
|
||||||
title: 'The Eminence in Shadow',
|
title: 'The Eminence in Shadow',
|
||||||
|
season: null,
|
||||||
episode: 5,
|
episode: 5,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface CliCommandRuntimeServiceContext {
|
|||||||
retryAnilistQueueNow: CliCommandRuntimeServiceDepsParams['anilist']['retryQueueNow'];
|
retryAnilistQueueNow: CliCommandRuntimeServiceDepsParams['anilist']['retryQueueNow'];
|
||||||
generateCharacterDictionary: CliCommandRuntimeServiceDepsParams['dictionary']['generate'];
|
generateCharacterDictionary: CliCommandRuntimeServiceDepsParams['dictionary']['generate'];
|
||||||
openJellyfinSetup: CliCommandRuntimeServiceDepsParams['jellyfin']['openSetup'];
|
openJellyfinSetup: CliCommandRuntimeServiceDepsParams['jellyfin']['openSetup'];
|
||||||
|
runStatsCommand: CliCommandRuntimeServiceDepsParams['jellyfin']['runStatsCommand'];
|
||||||
runJellyfinCommand: CliCommandRuntimeServiceDepsParams['jellyfin']['runCommand'];
|
runJellyfinCommand: CliCommandRuntimeServiceDepsParams['jellyfin']['runCommand'];
|
||||||
openYomitanSettings: () => void;
|
openYomitanSettings: () => void;
|
||||||
cycleSecondarySubMode: () => void;
|
cycleSecondarySubMode: () => void;
|
||||||
@@ -101,6 +102,7 @@ function createCliCommandDepsFromContext(
|
|||||||
},
|
},
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
openSetup: context.openJellyfinSetup,
|
openSetup: context.openJellyfinSetup,
|
||||||
|
runStatsCommand: context.runStatsCommand,
|
||||||
runCommand: context.runJellyfinCommand,
|
runCommand: context.runJellyfinCommand,
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export interface MainIpcRuntimeServiceDepsParams {
|
|||||||
handleMpvCommand: IpcDepsRuntimeOptions['handleMpvCommand'];
|
handleMpvCommand: IpcDepsRuntimeOptions['handleMpvCommand'];
|
||||||
getKeybindings: IpcDepsRuntimeOptions['getKeybindings'];
|
getKeybindings: IpcDepsRuntimeOptions['getKeybindings'];
|
||||||
getConfiguredShortcuts: IpcDepsRuntimeOptions['getConfiguredShortcuts'];
|
getConfiguredShortcuts: IpcDepsRuntimeOptions['getConfiguredShortcuts'];
|
||||||
|
getStatsToggleKey: IpcDepsRuntimeOptions['getStatsToggleKey'];
|
||||||
getControllerConfig: IpcDepsRuntimeOptions['getControllerConfig'];
|
getControllerConfig: IpcDepsRuntimeOptions['getControllerConfig'];
|
||||||
saveControllerPreference: IpcDepsRuntimeOptions['saveControllerPreference'];
|
saveControllerPreference: IpcDepsRuntimeOptions['saveControllerPreference'];
|
||||||
getSecondarySubMode: IpcDepsRuntimeOptions['getSecondarySubMode'];
|
getSecondarySubMode: IpcDepsRuntimeOptions['getSecondarySubMode'];
|
||||||
@@ -88,6 +89,7 @@ export interface MainIpcRuntimeServiceDepsParams {
|
|||||||
getAnilistQueueStatus: IpcDepsRuntimeOptions['getAnilistQueueStatus'];
|
getAnilistQueueStatus: IpcDepsRuntimeOptions['getAnilistQueueStatus'];
|
||||||
retryAnilistQueueNow: IpcDepsRuntimeOptions['retryAnilistQueueNow'];
|
retryAnilistQueueNow: IpcDepsRuntimeOptions['retryAnilistQueueNow'];
|
||||||
appendClipboardVideoToQueue: IpcDepsRuntimeOptions['appendClipboardVideoToQueue'];
|
appendClipboardVideoToQueue: IpcDepsRuntimeOptions['appendClipboardVideoToQueue'];
|
||||||
|
getImmersionTracker?: IpcDepsRuntimeOptions['getImmersionTracker'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
||||||
@@ -158,6 +160,7 @@ export interface CliCommandRuntimeServiceDepsParams {
|
|||||||
};
|
};
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
openSetup: CliCommandDepsRuntimeOptions['jellyfin']['openSetup'];
|
openSetup: CliCommandDepsRuntimeOptions['jellyfin']['openSetup'];
|
||||||
|
runStatsCommand: CliCommandDepsRuntimeOptions['jellyfin']['runStatsCommand'];
|
||||||
runCommand: CliCommandDepsRuntimeOptions['jellyfin']['runCommand'];
|
runCommand: CliCommandDepsRuntimeOptions['jellyfin']['runCommand'];
|
||||||
};
|
};
|
||||||
ui: {
|
ui: {
|
||||||
@@ -215,6 +218,7 @@ export function createMainIpcRuntimeServiceDeps(
|
|||||||
handleMpvCommand: params.handleMpvCommand,
|
handleMpvCommand: params.handleMpvCommand,
|
||||||
getKeybindings: params.getKeybindings,
|
getKeybindings: params.getKeybindings,
|
||||||
getConfiguredShortcuts: params.getConfiguredShortcuts,
|
getConfiguredShortcuts: params.getConfiguredShortcuts,
|
||||||
|
getStatsToggleKey: params.getStatsToggleKey,
|
||||||
getControllerConfig: params.getControllerConfig,
|
getControllerConfig: params.getControllerConfig,
|
||||||
saveControllerPreference: params.saveControllerPreference,
|
saveControllerPreference: params.saveControllerPreference,
|
||||||
focusMainWindow: params.focusMainWindow ?? (() => {}),
|
focusMainWindow: params.focusMainWindow ?? (() => {}),
|
||||||
@@ -232,6 +236,7 @@ export function createMainIpcRuntimeServiceDeps(
|
|||||||
getAnilistQueueStatus: params.getAnilistQueueStatus,
|
getAnilistQueueStatus: params.getAnilistQueueStatus,
|
||||||
retryAnilistQueueNow: params.retryAnilistQueueNow,
|
retryAnilistQueueNow: params.retryAnilistQueueNow,
|
||||||
appendClipboardVideoToQueue: params.appendClipboardVideoToQueue,
|
appendClipboardVideoToQueue: params.appendClipboardVideoToQueue,
|
||||||
|
getImmersionTracker: params.getImmersionTracker,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +315,7 @@ export function createCliCommandRuntimeServiceDeps(
|
|||||||
},
|
},
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
openSetup: params.jellyfin.openSetup,
|
openSetup: params.jellyfin.openSetup,
|
||||||
|
runStatsCommand: params.jellyfin.runStatsCommand,
|
||||||
runCommand: params.jellyfin.runCommand,
|
runCommand: params.jellyfin.runCommand,
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ test('ensure anilist media guess main deps builder maps callbacks', async () =>
|
|||||||
getCurrentMediaTitle: () => 'title',
|
getCurrentMediaTitle: () => 'title',
|
||||||
guessAnilistMediaInfo: async () => {
|
guessAnilistMediaInfo: async () => {
|
||||||
calls.push('guess');
|
calls.push('guess');
|
||||||
return { title: 'title', episode: 1, source: 'fallback' };
|
return { title: 'title', season: null, episode: 1, source: 'fallback' };
|
||||||
},
|
},
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ test('ensure anilist media guess main deps builder maps callbacks', async () =>
|
|||||||
assert.equal(deps.resolveMediaPathForJimaku('/tmp/video.mkv'), '/tmp/video.mkv');
|
assert.equal(deps.resolveMediaPathForJimaku('/tmp/video.mkv'), '/tmp/video.mkv');
|
||||||
assert.deepEqual(await deps.guessAnilistMediaInfo('/tmp/video.mkv', 'title'), {
|
assert.deepEqual(await deps.guessAnilistMediaInfo('/tmp/video.mkv', 'title'), {
|
||||||
title: 'title',
|
title: 'title',
|
||||||
|
season: null,
|
||||||
episode: 1,
|
episode: 1,
|
||||||
source: 'fallback',
|
source: 'fallback',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test('ensureAnilistMediaGuess memoizes in-flight guess promise', async () => {
|
|||||||
getCurrentMediaTitle: () => 'Episode 1',
|
getCurrentMediaTitle: () => 'Episode 1',
|
||||||
guessAnilistMediaInfo: async () => {
|
guessAnilistMediaInfo: async () => {
|
||||||
calls += 1;
|
calls += 1;
|
||||||
return { title: 'Show', episode: 1, source: 'guessit' };
|
return { title: 'Show', season: null, episode: 1, source: 'guessit' };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,9 +57,9 @@ test('ensureAnilistMediaGuess memoizes in-flight guess promise', async () => {
|
|||||||
ensureGuess('/tmp/video.mkv'),
|
ensureGuess('/tmp/video.mkv'),
|
||||||
ensureGuess('/tmp/video.mkv'),
|
ensureGuess('/tmp/video.mkv'),
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(first, { title: 'Show', episode: 1, source: 'guessit' });
|
assert.deepEqual(first, { title: 'Show', season: null, episode: 1, source: 'guessit' });
|
||||||
assert.deepEqual(second, { title: 'Show', episode: 1, source: 'guessit' });
|
assert.deepEqual(second, { title: 'Show', season: null, episode: 1, source: 'guessit' });
|
||||||
assert.equal(calls, 1);
|
assert.equal(calls, 1);
|
||||||
assert.deepEqual(state.mediaGuess, { title: 'Show', episode: 1, source: 'guessit' });
|
assert.deepEqual(state.mediaGuess, { title: 'Show', season: null, episode: 1, source: 'guessit' });
|
||||||
assert.equal(state.mediaGuessPromise, null);
|
assert.equal(state.mediaGuessPromise, null);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
test('process next anilist retry update main deps builder maps callbacks', async () => {
|
test('process next anilist retry update main deps builder maps callbacks', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const deps = createBuildProcessNextAnilistRetryUpdateMainDepsHandler({
|
const deps = createBuildProcessNextAnilistRetryUpdateMainDepsHandler({
|
||||||
nextReady: () => ({ key: 'k', title: 't', episode: 1 }),
|
nextReady: () => ({ key: 'k', title: 't', season: null, episode: 1 }),
|
||||||
refreshRetryQueueState: () => calls.push('refresh'),
|
refreshRetryQueueState: () => calls.push('refresh'),
|
||||||
setLastAttemptAt: () => calls.push('attempt'),
|
setLastAttemptAt: () => calls.push('attempt'),
|
||||||
setLastError: () => calls.push('error'),
|
setLastError: () => calls.push('error'),
|
||||||
@@ -59,7 +59,7 @@ test('maybe run anilist post watch update main deps builder maps callbacks', asy
|
|||||||
resetTrackedMedia: () => calls.push('reset'),
|
resetTrackedMedia: () => calls.push('reset'),
|
||||||
getWatchedSeconds: () => 100,
|
getWatchedSeconds: () => 100,
|
||||||
maybeProbeAnilistDuration: async () => 120,
|
maybeProbeAnilistDuration: async () => 120,
|
||||||
ensureAnilistMediaGuess: async () => ({ title: 'x', episode: 1 }),
|
ensureAnilistMediaGuess: async () => ({ title: 'x', season: null, episode: 1 }),
|
||||||
hasAttemptedUpdateKey: () => false,
|
hasAttemptedUpdateKey: () => false,
|
||||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
||||||
refreshAnilistClientSecretState: async () => 'token',
|
refreshAnilistClientSecretState: async () => 'token',
|
||||||
@@ -85,7 +85,7 @@ test('maybe run anilist post watch update main deps builder maps callbacks', asy
|
|||||||
deps.resetTrackedMedia('media');
|
deps.resetTrackedMedia('media');
|
||||||
assert.equal(deps.getWatchedSeconds(), 100);
|
assert.equal(deps.getWatchedSeconds(), 100);
|
||||||
assert.equal(await deps.maybeProbeAnilistDuration('media'), 120);
|
assert.equal(await deps.maybeProbeAnilistDuration('media'), 120);
|
||||||
assert.deepEqual(await deps.ensureAnilistMediaGuess('media'), { title: 'x', episode: 1 });
|
assert.deepEqual(await deps.ensureAnilistMediaGuess('media'), { title: 'x', season: null, episode: 1 });
|
||||||
assert.equal(deps.hasAttemptedUpdateKey('k'), false);
|
assert.equal(deps.hasAttemptedUpdateKey('k'), false);
|
||||||
assert.deepEqual(await deps.processNextAnilistRetryUpdate(), { ok: true, message: 'ok' });
|
assert.deepEqual(await deps.processNextAnilistRetryUpdate(), { ok: true, message: 'ok' });
|
||||||
assert.equal(await deps.refreshAnilistClientSecretState(), 'token');
|
assert.equal(await deps.refreshAnilistClientSecretState(), 'token');
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ test('rememberAnilistAttemptedUpdateKey evicts oldest beyond max size', () => {
|
|||||||
test('createProcessNextAnilistRetryUpdateHandler handles successful retry', async () => {
|
test('createProcessNextAnilistRetryUpdateHandler handles successful retry', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const handler = createProcessNextAnilistRetryUpdateHandler({
|
const handler = createProcessNextAnilistRetryUpdateHandler({
|
||||||
nextReady: () => ({ key: 'k1', title: 'Show', episode: 1 }),
|
nextReady: () => ({ key: 'k1', title: 'Show', season: null, episode: 1 }),
|
||||||
refreshRetryQueueState: () => calls.push('refresh'),
|
refreshRetryQueueState: () => calls.push('refresh'),
|
||||||
setLastAttemptAt: () => calls.push('attempt'),
|
setLastAttemptAt: () => calls.push('attempt'),
|
||||||
setLastError: (value) => calls.push(`error:${value ?? 'null'}`),
|
setLastError: (value) => calls.push(`error:${value ?? 'null'}`),
|
||||||
@@ -52,7 +52,7 @@ test('createMaybeRunAnilistPostWatchUpdateHandler queues when token missing', as
|
|||||||
resetTrackedMedia: () => {},
|
resetTrackedMedia: () => {},
|
||||||
getWatchedSeconds: () => 1000,
|
getWatchedSeconds: () => 1000,
|
||||||
maybeProbeAnilistDuration: async () => 1000,
|
maybeProbeAnilistDuration: async () => 1000,
|
||||||
ensureAnilistMediaGuess: async () => ({ title: 'Show', episode: 1 }),
|
ensureAnilistMediaGuess: async () => ({ title: 'Show', season: null, episode: 1 }),
|
||||||
hasAttemptedUpdateKey: () => false,
|
hasAttemptedUpdateKey: () => false,
|
||||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'noop' }),
|
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'noop' }),
|
||||||
refreshAnilistClientSecretState: async () => null,
|
refreshAnilistClientSecretState: async () => null,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export function createBuildAppReadyRuntimeMainDepsHandler(deps: AppReadyRuntimeD
|
|||||||
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
||||||
logDebug: deps.logDebug,
|
logDebug: deps.logDebug,
|
||||||
now: deps.now,
|
now: deps.now,
|
||||||
|
shouldUseMinimalStartup: deps.shouldUseMinimalStartup,
|
||||||
shouldSkipHeavyStartup: deps.shouldSkipHeavyStartup,
|
shouldSkipHeavyStartup: deps.shouldSkipHeavyStartup,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ test('build cli command context deps maps handlers and values', () => {
|
|||||||
mediaTitle: 'Test',
|
mediaTitle: 'Test',
|
||||||
entryCount: 10,
|
entryCount: 10,
|
||||||
}),
|
}),
|
||||||
|
runStatsCommand: async () => {
|
||||||
|
calls.push('run-stats');
|
||||||
|
},
|
||||||
runJellyfinCommand: async () => {
|
runJellyfinCommand: async () => {
|
||||||
calls.push('run-jellyfin');
|
calls.push('run-jellyfin');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
|
|||||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||||
retryAnilistQueueNow: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
retryAnilistQueueNow: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||||
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
|
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
|
||||||
|
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
|
||||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||||
openYomitanSettings: () => void;
|
openYomitanSettings: () => void;
|
||||||
cycleSecondarySubMode: () => void;
|
cycleSecondarySubMode: () => void;
|
||||||
@@ -80,6 +81,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
|
|||||||
getAnilistQueueStatus: deps.getAnilistQueueStatus,
|
getAnilistQueueStatus: deps.getAnilistQueueStatus,
|
||||||
retryAnilistQueueNow: deps.retryAnilistQueueNow,
|
retryAnilistQueueNow: deps.retryAnilistQueueNow,
|
||||||
generateCharacterDictionary: deps.generateCharacterDictionary,
|
generateCharacterDictionary: deps.generateCharacterDictionary,
|
||||||
|
runStatsCommand: deps.runStatsCommand,
|
||||||
runJellyfinCommand: deps.runJellyfinCommand,
|
runJellyfinCommand: deps.runJellyfinCommand,
|
||||||
openYomitanSettings: deps.openYomitanSettings,
|
openYomitanSettings: deps.openYomitanSettings,
|
||||||
cycleSecondarySubMode: deps.cycleSecondarySubMode,
|
cycleSecondarySubMode: deps.cycleSecondarySubMode,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ test('cli command context factory composes main deps and context handlers', () =
|
|||||||
mediaTitle: 'Test',
|
mediaTitle: 'Test',
|
||||||
entryCount: 10,
|
entryCount: 10,
|
||||||
}),
|
}),
|
||||||
|
runStatsCommand: async () => {},
|
||||||
runJellyfinCommand: async () => {},
|
runJellyfinCommand: async () => {},
|
||||||
openYomitanSettings: () => {},
|
openYomitanSettings: () => {},
|
||||||
cycleSecondarySubMode: () => {},
|
cycleSecondarySubMode: () => {},
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
|||||||
mediaTitle: 'Test',
|
mediaTitle: 'Test',
|
||||||
entryCount: 10,
|
entryCount: 10,
|
||||||
}),
|
}),
|
||||||
|
runStatsCommand: async () => {
|
||||||
|
calls.push('run-stats');
|
||||||
|
},
|
||||||
runJellyfinCommand: async () => {
|
runJellyfinCommand: async () => {
|
||||||
calls.push('run-jellyfin');
|
calls.push('run-jellyfin');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
|||||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||||
processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||||
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
|
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
|
||||||
|
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
|
||||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||||
|
|
||||||
openYomitanSettings: () => void;
|
openYomitanSettings: () => void;
|
||||||
@@ -92,6 +93,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
|||||||
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
|
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
|
||||||
generateCharacterDictionary: (targetPath?: string) =>
|
generateCharacterDictionary: (targetPath?: string) =>
|
||||||
deps.generateCharacterDictionary(targetPath),
|
deps.generateCharacterDictionary(targetPath),
|
||||||
|
runStatsCommand: (args: CliArgs, source) => deps.runStatsCommand(args, source),
|
||||||
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
|
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
|
||||||
openYomitanSettings: () => deps.openYomitanSettings(),
|
openYomitanSettings: () => deps.openYomitanSettings(),
|
||||||
cycleSecondarySubMode: () => deps.cycleSecondarySubMode(),
|
cycleSecondarySubMode: () => deps.cycleSecondarySubMode(),
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ function createDeps() {
|
|||||||
mediaTitle: 'Test',
|
mediaTitle: 'Test',
|
||||||
entryCount: 1,
|
entryCount: 1,
|
||||||
}),
|
}),
|
||||||
|
runStatsCommand: async () => {},
|
||||||
runJellyfinCommand: async () => {},
|
runJellyfinCommand: async () => {},
|
||||||
openYomitanSettings: () => {},
|
openYomitanSettings: () => {},
|
||||||
cycleSecondarySubMode: () => {},
|
cycleSecondarySubMode: () => {},
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type CliCommandContextFactoryDeps = {
|
|||||||
getAnilistQueueStatus: CliCommandRuntimeServiceContext['getAnilistQueueStatus'];
|
getAnilistQueueStatus: CliCommandRuntimeServiceContext['getAnilistQueueStatus'];
|
||||||
retryAnilistQueueNow: CliCommandRuntimeServiceContext['retryAnilistQueueNow'];
|
retryAnilistQueueNow: CliCommandRuntimeServiceContext['retryAnilistQueueNow'];
|
||||||
generateCharacterDictionary: CliCommandRuntimeServiceContext['generateCharacterDictionary'];
|
generateCharacterDictionary: CliCommandRuntimeServiceContext['generateCharacterDictionary'];
|
||||||
|
runStatsCommand: CliCommandRuntimeServiceContext['runStatsCommand'];
|
||||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||||
openYomitanSettings: () => void;
|
openYomitanSettings: () => void;
|
||||||
cycleSecondarySubMode: () => void;
|
cycleSecondarySubMode: () => void;
|
||||||
@@ -92,6 +93,7 @@ export function createCliCommandContext(
|
|||||||
getAnilistQueueStatus: deps.getAnilistQueueStatus,
|
getAnilistQueueStatus: deps.getAnilistQueueStatus,
|
||||||
retryAnilistQueueNow: deps.retryAnilistQueueNow,
|
retryAnilistQueueNow: deps.retryAnilistQueueNow,
|
||||||
generateCharacterDictionary: deps.generateCharacterDictionary,
|
generateCharacterDictionary: deps.generateCharacterDictionary,
|
||||||
|
runStatsCommand: deps.runStatsCommand,
|
||||||
runJellyfinCommand: deps.runJellyfinCommand,
|
runJellyfinCommand: deps.runJellyfinCommand,
|
||||||
openYomitanSettings: deps.openYomitanSettings,
|
openYomitanSettings: deps.openYomitanSettings,
|
||||||
cycleSecondarySubMode: deps.cycleSecondarySubMode,
|
cycleSecondarySubMode: deps.cycleSecondarySubMode,
|
||||||
|
|||||||
@@ -131,11 +131,11 @@ test('composeAnilistTrackingHandlers returns callable handlers and forwards call
|
|||||||
getCurrentMediaTitle: () => 'Episode title',
|
getCurrentMediaTitle: () => 'Episode title',
|
||||||
guessAnilistMediaInfo: async () => {
|
guessAnilistMediaInfo: async () => {
|
||||||
guessAnilistMediaInfoCalls += 1;
|
guessAnilistMediaInfoCalls += 1;
|
||||||
return { title: 'Episode title', episode: 7, source: 'guessit' };
|
return { title: 'Episode title', season: null, episode: 7, source: 'guessit' };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
processNextRetryUpdateMainDeps: {
|
processNextRetryUpdateMainDeps: {
|
||||||
nextReady: () => ({ key: 'retry-key', title: 'Retry title', episode: 1 }),
|
nextReady: () => ({ key: 'retry-key', title: 'Retry title', season: null, episode: 1 }),
|
||||||
refreshRetryQueueState: () => {},
|
refreshRetryQueueState: () => {},
|
||||||
setLastAttemptAt: () => {},
|
setLastAttemptAt: () => {},
|
||||||
setLastError: () => {},
|
setLastError: () => {},
|
||||||
@@ -163,6 +163,7 @@ test('composeAnilistTrackingHandlers returns callable handlers and forwards call
|
|||||||
maybeProbeAnilistDuration: async () => 600,
|
maybeProbeAnilistDuration: async () => 600,
|
||||||
ensureAnilistMediaGuess: async () => ({
|
ensureAnilistMediaGuess: async () => ({
|
||||||
title: 'Episode title',
|
title: 'Episode title',
|
||||||
|
season: null,
|
||||||
episode: 2,
|
episode: 2,
|
||||||
source: 'guessit',
|
source: 'guessit',
|
||||||
}),
|
}),
|
||||||
@@ -209,7 +210,7 @@ test('composeAnilistTrackingHandlers returns callable handlers and forwards call
|
|||||||
composed.setAnilistMediaGuessRuntimeState({
|
composed.setAnilistMediaGuessRuntimeState({
|
||||||
mediaKey: 'media-key',
|
mediaKey: 'media-key',
|
||||||
mediaDurationSec: 90,
|
mediaDurationSec: 90,
|
||||||
mediaGuess: { title: 'Known', episode: 3, source: 'fallback' },
|
mediaGuess: { title: 'Known', season: null, episode: 3, source: 'fallback' },
|
||||||
mediaGuessPromise: null,
|
mediaGuessPromise: null,
|
||||||
lastDurationProbeAtMs: 11,
|
lastDurationProbeAtMs: 11,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
|
|||||||
getMecabTokenizer: () => null,
|
getMecabTokenizer: () => null,
|
||||||
getKeybindings: () => [],
|
getKeybindings: () => [],
|
||||||
getConfiguredShortcuts: () => ({}) as never,
|
getConfiguredShortcuts: () => ({}) as never,
|
||||||
|
getStatsToggleKey: () => 'Backquote',
|
||||||
getControllerConfig: () => ({}) as never,
|
getControllerConfig: () => ({}) as never,
|
||||||
saveControllerPreference: () => {},
|
saveControllerPreference: () => {},
|
||||||
getSecondarySubMode: () => 'hover' as never,
|
getSecondarySubMode: () => 'hover' as never,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
|||||||
anilistSetup: false,
|
anilistSetup: false,
|
||||||
anilistRetryQueue: false,
|
anilistRetryQueue: false,
|
||||||
dictionary: false,
|
dictionary: false,
|
||||||
|
stats: false,
|
||||||
jellyfin: false,
|
jellyfin: false,
|
||||||
jellyfinLogin: false,
|
jellyfinLogin: false,
|
||||||
jellyfinLogout: false,
|
jellyfinLogout: false,
|
||||||
|
|||||||
@@ -135,3 +135,28 @@ test('createImmersionTrackerStartupHandler disables tracker on failure', () => {
|
|||||||
calls.includes('warn:Immersion tracker startup failed; disabling tracking.:db unavailable'),
|
calls.includes('warn:Immersion tracker startup failed; disabling tracking.:db unavailable'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createImmersionTrackerStartupHandler skips mpv auto-connect when disabled by caller', () => {
|
||||||
|
let connectCalls = 0;
|
||||||
|
const handler = createImmersionTrackerStartupHandler({
|
||||||
|
getResolvedConfig: () => makeConfig(),
|
||||||
|
getConfiguredDbPath: () => '/tmp/subminer.db',
|
||||||
|
createTrackerService: () => ({}),
|
||||||
|
setTracker: () => {},
|
||||||
|
getMpvClient: () => ({
|
||||||
|
connected: false,
|
||||||
|
connect: () => {
|
||||||
|
connectCalls += 1;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
shouldAutoConnectMpv: () => false,
|
||||||
|
seedTrackerFromCurrentMedia: () => {},
|
||||||
|
logInfo: () => {},
|
||||||
|
logDebug: () => {},
|
||||||
|
logWarn: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
handler();
|
||||||
|
|
||||||
|
assert.equal(connectCalls, 0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type ImmersionTrackerStartupDeps = {
|
|||||||
createTrackerService: (params: ImmersionTrackerServiceParams) => unknown;
|
createTrackerService: (params: ImmersionTrackerServiceParams) => unknown;
|
||||||
setTracker: (tracker: unknown | null) => void;
|
setTracker: (tracker: unknown | null) => void;
|
||||||
getMpvClient: () => MpvClientLike | null;
|
getMpvClient: () => MpvClientLike | null;
|
||||||
|
shouldAutoConnectMpv?: () => boolean;
|
||||||
seedTrackerFromCurrentMedia: () => void;
|
seedTrackerFromCurrentMedia: () => void;
|
||||||
logInfo: (message: string) => void;
|
logInfo: (message: string) => void;
|
||||||
logDebug: (message: string) => void;
|
logDebug: (message: string) => void;
|
||||||
@@ -86,7 +87,7 @@ export function createImmersionTrackerStartupHandler(
|
|||||||
deps.logDebug('Immersion tracker initialized successfully.');
|
deps.logDebug('Immersion tracker initialized successfully.');
|
||||||
|
|
||||||
const mpvClient = deps.getMpvClient();
|
const mpvClient = deps.getMpvClient();
|
||||||
if (mpvClient && !mpvClient.connected) {
|
if ((deps.shouldAutoConnectMpv?.() ?? true) && mpvClient && !mpvClient.connected) {
|
||||||
deps.logInfo('Auto-connecting MPV client for immersion tracking');
|
deps.logInfo('Auto-connecting MPV client for immersion tracking');
|
||||||
mpvClient.connect();
|
mpvClient.connect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function createHandleInitialArgsHandler(deps: {
|
|||||||
const mpvClient = deps.getMpvClient();
|
const mpvClient = deps.getMpvClient();
|
||||||
if (
|
if (
|
||||||
!deps.isTexthookerOnlyMode() &&
|
!deps.isTexthookerOnlyMode() &&
|
||||||
|
!initialArgs.stats &&
|
||||||
deps.hasImmersionTracker() &&
|
deps.hasImmersionTracker() &&
|
||||||
mpvClient &&
|
mpvClient &&
|
||||||
!mpvClient.connected
|
!mpvClient.connected
|
||||||
|
|||||||
@@ -28,3 +28,25 @@ test('initial args runtime handler composes main deps and runs initial command f
|
|||||||
'cli:initial',
|
'cli:initial',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('initial args runtime handler skips mpv auto-connect for stats mode', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const handleInitialArgs = createInitialArgsRuntimeHandler({
|
||||||
|
getInitialArgs: () => ({ stats: true }) as never,
|
||||||
|
isBackgroundMode: () => false,
|
||||||
|
shouldEnsureTrayOnStartup: () => false,
|
||||||
|
ensureTray: () => calls.push('tray'),
|
||||||
|
isTexthookerOnlyMode: () => false,
|
||||||
|
hasImmersionTracker: () => true,
|
||||||
|
getMpvClient: () => ({
|
||||||
|
connected: false,
|
||||||
|
connect: () => calls.push('connect'),
|
||||||
|
}),
|
||||||
|
logInfo: (message) => calls.push(`log:${message}`),
|
||||||
|
handleCliCommand: (_args, source) => calls.push(`cli:${source}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
handleInitialArgs();
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['cli:initial']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ test('mpv event bindings register all expected events', () => {
|
|||||||
onMediaPathChange: () => {},
|
onMediaPathChange: () => {},
|
||||||
onMediaTitleChange: () => {},
|
onMediaTitleChange: () => {},
|
||||||
onTimePosChange: () => {},
|
onTimePosChange: () => {},
|
||||||
|
onDurationChange: () => {},
|
||||||
onPauseChange: () => {},
|
onPauseChange: () => {},
|
||||||
onSubtitleMetricsChange: () => {},
|
onSubtitleMetricsChange: () => {},
|
||||||
onSecondarySubtitleVisibility: () => {},
|
onSecondarySubtitleVisibility: () => {},
|
||||||
@@ -95,6 +96,7 @@ test('mpv event bindings register all expected events', () => {
|
|||||||
'media-path-change',
|
'media-path-change',
|
||||||
'media-title-change',
|
'media-title-change',
|
||||||
'time-pos-change',
|
'time-pos-change',
|
||||||
|
'duration-change',
|
||||||
'pause-change',
|
'pause-change',
|
||||||
'subtitle-metrics-change',
|
'subtitle-metrics-change',
|
||||||
'secondary-subtitle-visibility',
|
'secondary-subtitle-visibility',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type MpvBindingEventName =
|
|||||||
| 'media-path-change'
|
| 'media-path-change'
|
||||||
| 'media-title-change'
|
| 'media-title-change'
|
||||||
| 'time-pos-change'
|
| 'time-pos-change'
|
||||||
|
| 'duration-change'
|
||||||
| 'pause-change'
|
| 'pause-change'
|
||||||
| 'subtitle-metrics-change'
|
| 'subtitle-metrics-change'
|
||||||
| 'secondary-subtitle-visibility';
|
| 'secondary-subtitle-visibility';
|
||||||
@@ -72,6 +73,7 @@ export function createBindMpvClientEventHandlers(deps: {
|
|||||||
onMediaPathChange: (payload: { path: string | null }) => void;
|
onMediaPathChange: (payload: { path: string | null }) => void;
|
||||||
onMediaTitleChange: (payload: { title: string | null }) => void;
|
onMediaTitleChange: (payload: { title: string | null }) => void;
|
||||||
onTimePosChange: (payload: { time: number }) => void;
|
onTimePosChange: (payload: { time: number }) => void;
|
||||||
|
onDurationChange: (payload: { duration: number }) => void;
|
||||||
onPauseChange: (payload: { paused: boolean }) => void;
|
onPauseChange: (payload: { paused: boolean }) => void;
|
||||||
onSubtitleMetricsChange: (payload: { patch: Record<string, unknown> }) => void;
|
onSubtitleMetricsChange: (payload: { patch: Record<string, unknown> }) => void;
|
||||||
onSecondarySubtitleVisibility: (payload: { visible: boolean }) => void;
|
onSecondarySubtitleVisibility: (payload: { visible: boolean }) => void;
|
||||||
@@ -85,6 +87,7 @@ export function createBindMpvClientEventHandlers(deps: {
|
|||||||
mpvClient.on('media-path-change', deps.onMediaPathChange);
|
mpvClient.on('media-path-change', deps.onMediaPathChange);
|
||||||
mpvClient.on('media-title-change', deps.onMediaTitleChange);
|
mpvClient.on('media-title-change', deps.onMediaTitleChange);
|
||||||
mpvClient.on('time-pos-change', deps.onTimePosChange);
|
mpvClient.on('time-pos-change', deps.onTimePosChange);
|
||||||
|
mpvClient.on('duration-change', deps.onDurationChange);
|
||||||
mpvClient.on('pause-change', deps.onPauseChange);
|
mpvClient.on('pause-change', deps.onPauseChange);
|
||||||
mpvClient.on('subtitle-metrics-change', deps.onSubtitleMetricsChange);
|
mpvClient.on('subtitle-metrics-change', deps.onSubtitleMetricsChange);
|
||||||
mpvClient.on('secondary-subtitle-visibility', deps.onSecondarySubtitleVisibility);
|
mpvClient.on('secondary-subtitle-visibility', deps.onSecondarySubtitleVisibility);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
|||||||
notifyImmersionTitleUpdate: (title) => calls.push(`notify-title:${title}`),
|
notifyImmersionTitleUpdate: (title) => calls.push(`notify-title:${title}`),
|
||||||
|
|
||||||
recordPlaybackPosition: (time) => calls.push(`time-pos:${time}`),
|
recordPlaybackPosition: (time) => calls.push(`time-pos:${time}`),
|
||||||
|
recordMediaDuration: (duration) => calls.push(`duration:${duration}`),
|
||||||
reportJellyfinRemoteProgress: (forceImmediate) =>
|
reportJellyfinRemoteProgress: (forceImmediate) =>
|
||||||
calls.push(`progress:${forceImmediate ? 'force' : 'normal'}`),
|
calls.push(`progress:${forceImmediate ? 'force' : 'normal'}`),
|
||||||
recordPauseState: (paused) => calls.push(`pause:${paused ? 'yes' : 'no'}`),
|
recordPauseState: (paused) => calls.push(`pause:${paused ? 'yes' : 'no'}`),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
notifyImmersionTitleUpdate: (title: string) => void;
|
notifyImmersionTitleUpdate: (title: string) => void;
|
||||||
|
|
||||||
recordPlaybackPosition: (time: number) => void;
|
recordPlaybackPosition: (time: number) => void;
|
||||||
|
recordMediaDuration: (durationSec: number) => void;
|
||||||
reportJellyfinRemoteProgress: (forceImmediate: boolean) => void;
|
reportJellyfinRemoteProgress: (forceImmediate: boolean) => void;
|
||||||
recordPauseState: (paused: boolean) => void;
|
recordPauseState: (paused: boolean) => void;
|
||||||
|
|
||||||
@@ -147,6 +148,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
onMediaPathChange: handleMpvMediaPathChange,
|
onMediaPathChange: handleMpvMediaPathChange,
|
||||||
onMediaTitleChange: handleMpvMediaTitleChange,
|
onMediaTitleChange: handleMpvMediaTitleChange,
|
||||||
onTimePosChange: handleMpvTimePosChange,
|
onTimePosChange: handleMpvTimePosChange,
|
||||||
|
onDurationChange: ({ duration }) => deps.recordMediaDuration(duration),
|
||||||
onPauseChange: handleMpvPauseChange,
|
onPauseChange: handleMpvPauseChange,
|
||||||
onSubtitleMetricsChange: handleMpvSubtitleMetricsChange,
|
onSubtitleMetricsChange: handleMpvSubtitleMetricsChange,
|
||||||
onSecondarySubtitleVisibility: handleMpvSecondarySubtitleVisibility,
|
onSecondarySubtitleVisibility: handleMpvSecondarySubtitleVisibility,
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
|
import type { MergedToken, SubtitleData } from '../../types';
|
||||||
|
|
||||||
export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||||
appState: {
|
appState: {
|
||||||
initialArgs?: { jellyfinPlay?: unknown } | null;
|
initialArgs?: { jellyfinPlay?: unknown } | null;
|
||||||
overlayRuntimeInitialized: boolean;
|
overlayRuntimeInitialized: boolean;
|
||||||
mpvClient: { connected?: boolean } | null;
|
mpvClient: { connected?: boolean } | null;
|
||||||
immersionTracker: {
|
immersionTracker: {
|
||||||
recordSubtitleLine?: (text: string, start: number, end: number) => void;
|
recordSubtitleLine?: (
|
||||||
|
text: string,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
tokens?: MergedToken[] | null,
|
||||||
|
) => void;
|
||||||
handleMediaTitleUpdate?: (title: string) => void;
|
handleMediaTitleUpdate?: (title: string) => void;
|
||||||
recordPlaybackPosition?: (time: number) => void;
|
recordPlaybackPosition?: (time: number) => void;
|
||||||
|
recordMediaDuration?: (durationSec: number) => void;
|
||||||
recordPauseState?: (paused: boolean) => void;
|
recordPauseState?: (paused: boolean) => void;
|
||||||
} | null;
|
} | null;
|
||||||
subtitleTimingTracker: {
|
subtitleTimingTracker: {
|
||||||
@@ -14,6 +22,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
} | null;
|
} | null;
|
||||||
currentSubText: string;
|
currentSubText: string;
|
||||||
currentSubAssText: string;
|
currentSubAssText: string;
|
||||||
|
currentSubtitleData?: SubtitleData | null;
|
||||||
playbackPaused: boolean | null;
|
playbackPaused: boolean | null;
|
||||||
previousSecondarySubVisibility: boolean | null;
|
previousSecondarySubVisibility: boolean | null;
|
||||||
};
|
};
|
||||||
@@ -41,6 +50,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) => void;
|
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) => void;
|
||||||
refreshDiscordPresence: () => void;
|
refreshDiscordPresence: () => void;
|
||||||
ensureImmersionTrackerInitialized: () => void;
|
ensureImmersionTrackerInitialized: () => void;
|
||||||
|
tokenizeSubtitleForImmersion?: (text: string) => Promise<SubtitleData | null>;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||||
@@ -53,7 +63,30 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
quitApp: () => deps.quitApp(),
|
quitApp: () => deps.quitApp(),
|
||||||
recordImmersionSubtitleLine: (text: string, start: number, end: number) => {
|
recordImmersionSubtitleLine: (text: string, start: number, end: number) => {
|
||||||
deps.ensureImmersionTrackerInitialized();
|
deps.ensureImmersionTrackerInitialized();
|
||||||
deps.appState.immersionTracker?.recordSubtitleLine?.(text, start, end);
|
const tracker = deps.appState.immersionTracker;
|
||||||
|
if (!tracker?.recordSubtitleLine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cachedTokens =
|
||||||
|
deps.appState.currentSubtitleData?.text === text
|
||||||
|
? deps.appState.currentSubtitleData.tokens
|
||||||
|
: null;
|
||||||
|
if (cachedTokens) {
|
||||||
|
tracker.recordSubtitleLine(text, start, end, cachedTokens);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!deps.tokenizeSubtitleForImmersion) {
|
||||||
|
tracker.recordSubtitleLine(text, start, end, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void deps
|
||||||
|
.tokenizeSubtitleForImmersion(text)
|
||||||
|
.then((payload) => {
|
||||||
|
tracker.recordSubtitleLine?.(text, start, end, payload?.tokens ?? null);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
tracker.recordSubtitleLine?.(text, start, end, null);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
hasSubtitleTimingTracker: () => Boolean(deps.appState.subtitleTimingTracker),
|
hasSubtitleTimingTracker: () => Boolean(deps.appState.subtitleTimingTracker),
|
||||||
recordSubtitleTiming: (text: string, start: number, end: number) =>
|
recordSubtitleTiming: (text: string, start: number, end: number) =>
|
||||||
@@ -95,6 +128,10 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
deps.ensureImmersionTrackerInitialized();
|
deps.ensureImmersionTrackerInitialized();
|
||||||
deps.appState.immersionTracker?.recordPlaybackPosition?.(time);
|
deps.appState.immersionTracker?.recordPlaybackPosition?.(time);
|
||||||
},
|
},
|
||||||
|
recordMediaDuration: (durationSec: number) => {
|
||||||
|
deps.ensureImmersionTrackerInitialized();
|
||||||
|
deps.appState.immersionTracker?.recordMediaDuration?.(durationSec);
|
||||||
|
},
|
||||||
reportJellyfinRemoteProgress: (forceImmediate: boolean) =>
|
reportJellyfinRemoteProgress: (forceImmediate: boolean) =>
|
||||||
deps.reportJellyfinRemoteProgress(forceImmediate),
|
deps.reportJellyfinRemoteProgress(forceImmediate),
|
||||||
recordPauseState: (paused: boolean) => {
|
recordPauseState: (paused: boolean) => {
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ export interface AppState {
|
|||||||
anilistSetupPageOpened: boolean;
|
anilistSetupPageOpened: boolean;
|
||||||
anilistRetryQueueState: AnilistRetryQueueState;
|
anilistRetryQueueState: AnilistRetryQueueState;
|
||||||
firstRunSetupCompleted: boolean;
|
firstRunSetupCompleted: boolean;
|
||||||
|
statsServer: { close: () => void } | null;
|
||||||
|
statsStartupInProgress: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppStateInitialValues {
|
export interface AppStateInitialValues {
|
||||||
@@ -275,6 +277,8 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
|||||||
anilistSetupPageOpened: false,
|
anilistSetupPageOpened: false,
|
||||||
anilistRetryQueueState: createInitialAnilistRetryQueueState(),
|
anilistRetryQueueState: createInitialAnilistRetryQueueState(),
|
||||||
firstRunSetupCompleted: false,
|
firstRunSetupCompleted: false,
|
||||||
|
statsServer: null,
|
||||||
|
statsStartupInProgress: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ function installKeyboardTestGlobals() {
|
|||||||
const commandEvents: CommandEventDetail[] = [];
|
const commandEvents: CommandEventDetail[] = [];
|
||||||
const mpvCommands: Array<Array<string | number>> = [];
|
const mpvCommands: Array<Array<string | number>> = [];
|
||||||
let playbackPausedResponse: boolean | null = false;
|
let playbackPausedResponse: boolean | null = false;
|
||||||
|
let statsToggleKey = 'Backquote';
|
||||||
|
let statsToggleOverlayCalls = 0;
|
||||||
let selectionClearCount = 0;
|
let selectionClearCount = 0;
|
||||||
let selectionAddCount = 0;
|
let selectionAddCount = 0;
|
||||||
|
|
||||||
@@ -137,7 +139,11 @@ function installKeyboardTestGlobals() {
|
|||||||
mpvCommands.push(command);
|
mpvCommands.push(command);
|
||||||
},
|
},
|
||||||
getPlaybackPaused: async () => playbackPausedResponse,
|
getPlaybackPaused: async () => playbackPausedResponse,
|
||||||
|
getStatsToggleKey: async () => statsToggleKey,
|
||||||
toggleDevTools: () => {},
|
toggleDevTools: () => {},
|
||||||
|
toggleStatsOverlay: () => {
|
||||||
|
statsToggleOverlayCalls += 1;
|
||||||
|
},
|
||||||
focusMainWindow: () => {
|
focusMainWindow: () => {
|
||||||
focusMainWindowCalls += 1;
|
focusMainWindowCalls += 1;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -253,6 +259,10 @@ function installKeyboardTestGlobals() {
|
|||||||
setPopupVisible: (value: boolean) => {
|
setPopupVisible: (value: boolean) => {
|
||||||
popupVisible = value;
|
popupVisible = value;
|
||||||
},
|
},
|
||||||
|
setStatsToggleKey: (value: string) => {
|
||||||
|
statsToggleKey = value;
|
||||||
|
},
|
||||||
|
statsToggleOverlayCalls: () => statsToggleOverlayCalls,
|
||||||
getPlaybackPaused: async () => playbackPausedResponse,
|
getPlaybackPaused: async () => playbackPausedResponse,
|
||||||
setPlaybackPausedResponse: (value: boolean | null) => {
|
setPlaybackPausedResponse: (value: boolean | null) => {
|
||||||
playbackPausedResponse = value;
|
playbackPausedResponse = value;
|
||||||
@@ -548,6 +558,22 @@ test('keyboard mode: controller select modal handles arrow keys before yomitan p
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keyboard mode: configured stats toggle works even while popup is open', async () => {
|
||||||
|
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
testGlobals.setPopupVisible(true);
|
||||||
|
testGlobals.setStatsToggleKey('KeyG');
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
|
testGlobals.dispatchKeydown({ key: 'g', code: 'KeyG' });
|
||||||
|
|
||||||
|
assert.equal(testGlobals.statsToggleOverlayCalls(), 1);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('keyboard mode: h moves left when popup is closed', async () => {
|
test('keyboard mode: h moves left when popup is closed', async () => {
|
||||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,17 @@ export function createKeyboardHandlers(
|
|||||||
return !e.ctrlKey && !e.metaKey && e.altKey && !e.repeat && e.code === 'KeyC';
|
return !e.ctrlKey && !e.metaKey && e.altKey && !e.repeat && e.code === 'KeyC';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStatsOverlayToggle(e: KeyboardEvent): boolean {
|
||||||
|
return (
|
||||||
|
e.code === ctx.state.statsToggleKey &&
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.altKey &&
|
||||||
|
!e.metaKey &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
!e.repeat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getSubtitleWordNodes(): HTMLElement[] {
|
function getSubtitleWordNodes(): HTMLElement[] {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
ctx.dom.subtitleRoot.querySelectorAll<HTMLElement>('.word[data-token-index]'),
|
ctx.dom.subtitleRoot.querySelectorAll<HTMLElement>('.word[data-token-index]'),
|
||||||
@@ -693,7 +704,12 @@ export function createKeyboardHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupMpvInputForwarding(): Promise<void> {
|
async function setupMpvInputForwarding(): Promise<void> {
|
||||||
updateKeybindings(await window.electronAPI.getKeybindings());
|
const [keybindings, statsToggleKey] = await Promise.all([
|
||||||
|
window.electronAPI.getKeybindings(),
|
||||||
|
window.electronAPI.getStatsToggleKey(),
|
||||||
|
]);
|
||||||
|
updateKeybindings(keybindings);
|
||||||
|
ctx.state.statsToggleKey = statsToggleKey;
|
||||||
syncKeyboardTokenSelection();
|
syncKeyboardTokenSelection();
|
||||||
|
|
||||||
const subtitleMutationObserver = new MutationObserver(() => {
|
const subtitleMutationObserver = new MutationObserver(() => {
|
||||||
@@ -789,6 +805,12 @@ export function createKeyboardHandlers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isStatsOverlayToggle(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.electronAPI.toggleStatsOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) &&
|
(ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) &&
|
||||||
!isControllerModalShortcut(e)
|
!isControllerModalShortcut(e)
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export type RendererState = {
|
|||||||
frequencyDictionaryBand5Color: string;
|
frequencyDictionaryBand5Color: string;
|
||||||
|
|
||||||
keybindingsMap: Map<string, (string | number)[]>;
|
keybindingsMap: Map<string, (string | number)[]>;
|
||||||
|
statsToggleKey: string;
|
||||||
chordPending: boolean;
|
chordPending: boolean;
|
||||||
chordTimeout: ReturnType<typeof setTimeout> | null;
|
chordTimeout: ReturnType<typeof setTimeout> | null;
|
||||||
keyboardDrivenModeEnabled: boolean;
|
keyboardDrivenModeEnabled: boolean;
|
||||||
@@ -170,6 +171,7 @@ export function createRendererState(): RendererState {
|
|||||||
frequencyDictionaryBand5Color: '#8aadf4',
|
frequencyDictionaryBand5Color: '#8aadf4',
|
||||||
|
|
||||||
keybindingsMap: new Map(),
|
keybindingsMap: new Map(),
|
||||||
|
statsToggleKey: 'Backquote',
|
||||||
chordPending: false,
|
chordPending: false,
|
||||||
chordTimeout: null,
|
chordTimeout: null,
|
||||||
keyboardDrivenModeEnabled: false,
|
keyboardDrivenModeEnabled: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user