fix(macos): keep overlay interactive when mpv loses foreground

- Track overlay mouse interaction state via IPC setIgnoreMouseEvents hook
- Skip macOS hide/passthrough when overlayInteractionActive is set
- Focus overlay window so lookup keys reach it during interaction
- Record mpv duration events into AniList media state for threshold checks
This commit is contained in:
2026-05-16 18:48:45 -07:00
parent 215e0f804b
commit fe201a2d2f
18 changed files with 425 additions and 4 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
type: fixed type: fixed
area: anilist area: anilist
- Used fresh mpv time-position events for AniList post-watch threshold checks so progress updates still fire when playback reaches the watched threshold. - Used fresh mpv time-position and duration events for AniList post-watch threshold checks so progress updates still fire when playback reaches or skips past the watched threshold.
+10 -1
View File
@@ -218,6 +218,9 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
getMainWindow: () => null, getMainWindow: () => null,
getVisibleOverlayVisibility: () => false, getVisibleOverlayVisibility: () => false,
onOverlayModalClosed: () => {}, onOverlayModalClosed: () => {},
onOverlayMouseInteractionChanged: (active) => {
calls.push(`overlay-interaction:${active}`);
},
openYomitanSettings: () => {}, openYomitanSettings: () => {},
quitApp: () => {}, quitApp: () => {},
toggleVisibleOverlay: () => {}, toggleVisibleOverlay: () => {},
@@ -281,6 +284,7 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
assert.deepEqual(deps.getAnilistStatus(), { tokenStatus: 'resolved' }); assert.deepEqual(deps.getAnilistStatus(), { tokenStatus: 'resolved' });
deps.clearAnilistToken(); deps.clearAnilistToken();
deps.openAnilistSetup(); deps.openAnilistSetup();
deps.onOverlayMouseInteractionChanged?.(true, null);
assert.deepEqual(deps.getAnilistQueueStatus(), { assert.deepEqual(deps.getAnilistQueueStatus(), {
pending: 1, pending: 1,
ready: 0, ready: 0,
@@ -298,7 +302,12 @@ test('createIpcDepsRuntime wires AniList handlers', async () => {
assert.deepEqual(await deps.playPlaylistBrowserIndex(2), { ok: true, message: 'play' }); assert.deepEqual(await deps.playPlaylistBrowserIndex(2), { ok: true, message: 'play' });
assert.deepEqual(await deps.removePlaylistBrowserIndex(2), { ok: true, message: 'remove' }); assert.deepEqual(await deps.removePlaylistBrowserIndex(2), { ok: true, message: 'remove' });
assert.deepEqual(await deps.movePlaylistBrowserIndex(2, -1), { ok: true, message: 'move' }); assert.deepEqual(await deps.movePlaylistBrowserIndex(2, -1), { ok: true, message: 'move' });
assert.deepEqual(calls, ['clearAnilistToken', 'openAnilistSetup', 'retryAnilistQueueNow']); assert.deepEqual(calls, [
'clearAnilistToken',
'openAnilistSetup',
'overlay-interaction:true',
'retryAnilistQueueNow',
]);
assert.equal(deps.getPlaybackPaused(), true); assert.equal(deps.getPlaybackPaused(), true);
}); });
+10
View File
@@ -44,6 +44,10 @@ export interface IpcServiceDeps {
modal: OverlayHostedModal, modal: OverlayHostedModal,
senderWindow: ElectronBrowserWindow | null, senderWindow: ElectronBrowserWindow | null,
) => void; ) => void;
onOverlayMouseInteractionChanged?: (
active: boolean,
senderWindow: ElectronBrowserWindow | null,
) => void;
openYomitanSettings: () => void; openYomitanSettings: () => void;
quitApp: () => void; quitApp: () => void;
toggleDevTools: () => void; toggleDevTools: () => void;
@@ -175,6 +179,10 @@ export interface IpcDepsRuntimeOptions {
modal: OverlayHostedModal, modal: OverlayHostedModal,
senderWindow: ElectronBrowserWindow | null, senderWindow: ElectronBrowserWindow | null,
) => void; ) => void;
onOverlayMouseInteractionChanged?: (
active: boolean,
senderWindow: ElectronBrowserWindow | null,
) => void;
openYomitanSettings: () => void; openYomitanSettings: () => void;
quitApp: () => void; quitApp: () => void;
toggleVisibleOverlay: () => void; toggleVisibleOverlay: () => void;
@@ -233,6 +241,7 @@ export function createIpcDepsRuntime(options: IpcDepsRuntimeOptions): IpcService
return { return {
onOverlayModalClosed: options.onOverlayModalClosed, onOverlayModalClosed: options.onOverlayModalClosed,
onOverlayModalOpened: options.onOverlayModalOpened, onOverlayModalOpened: options.onOverlayModalOpened,
onOverlayMouseInteractionChanged: options.onOverlayMouseInteractionChanged,
openYomitanSettings: options.openYomitanSettings, openYomitanSettings: options.openYomitanSettings,
quitApp: options.quitApp, quitApp: options.quitApp,
toggleDevTools: () => { toggleDevTools: () => {
@@ -349,6 +358,7 @@ export function registerIpcHandlers(deps: IpcServiceDeps, ipc: IpcMainRegistrar
if (senderWindow && !senderWindow.isDestroyed()) { if (senderWindow && !senderWindow.isDestroyed()) {
senderWindow.setIgnoreMouseEvents(ignore, parsedOptions); senderWindow.setIgnoreMouseEvents(ignore, parsedOptions);
} }
deps.onOverlayMouseInteractionChanged?.(!ignore, senderWindow);
}, },
); );
@@ -1110,6 +1110,140 @@ test('macOS tracked overlay hides when mpv loses foreground', () => {
assert.ok(!calls.includes('show')); assert.ok(!calls.includes('show'));
}); });
test('macOS keeps tracked overlay visible while overlay interaction is active after mpv loses foreground', () => {
const { window, calls, setFocused } = createMainWindowRecorder();
const tracker: WindowTrackerStub = {
isTracking: () => true,
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
isTargetWindowFocused: () => false,
};
window.show();
setFocused(false);
calls.length = 0;
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
overlayInteractionActive: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: false,
setTrackerNotReadyWarningShown: () => {},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
isWindowsPlatform: false,
} as never);
assert.ok(calls.includes('update-bounds'));
assert.ok(calls.includes('sync-layer'));
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(calls.includes('ensure-level'));
assert.ok(calls.includes('enforce-order'));
assert.ok(calls.includes('sync-shortcuts'));
assert.ok(!calls.includes('always-on-top:false'));
assert.ok(!calls.includes('hide'));
});
test('macOS lets an active overlay receive mouse input instead of forcing passthrough', () => {
const { window, calls, setFocused } = createMainWindowRecorder();
const tracker: WindowTrackerStub = {
isTracking: () => true,
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
isTargetWindowFocused: () => false,
};
window.show();
setFocused(false);
calls.length = 0;
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
overlayInteractionActive: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: false,
setTrackerNotReadyWarningShown: () => {},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
isWindowsPlatform: false,
} as never);
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(!calls.includes('mouse-ignore:true:forward'));
assert.ok(!calls.includes('hide'));
});
test('macOS focuses an active overlay so lookup trigger keys reach it', () => {
const { window, calls, setFocused } = createMainWindowRecorder();
const tracker: WindowTrackerStub = {
isTracking: () => true,
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
isTargetWindowFocused: () => false,
};
window.show();
setFocused(false);
calls.length = 0;
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
overlayInteractionActive: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: false,
setTrackerNotReadyWarningShown: () => {},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
isWindowsPlatform: false,
} as never);
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(calls.includes('focus'));
assert.ok(!calls.includes('hide'));
});
test('macOS tracked overlay passively reappears when mpv regains foreground', () => { test('macOS tracked overlay passively reappears when mpv regains foreground', () => {
const { window, calls } = createMainWindowRecorder(); const { window, calls } = createMainWindowRecorder();
let targetFocused = false; let targetFocused = false;
@@ -1647,6 +1781,57 @@ test('macOS keeps a focused overlay visible during tracker loss', () => {
assert.ok(!calls.includes('loading-osd')); assert.ok(!calls.includes('loading-osd'));
}); });
test('macOS keeps an interactive overlay visible during tracker loss even when Electron focus drops', () => {
const { window, calls, setFocused } = createMainWindowRecorder();
const tracker: WindowTrackerStub = {
isTracking: () => false,
getGeometry: () => null,
isTargetWindowFocused: () => false,
isTargetWindowMinimized: () => false,
};
window.show();
setFocused(false);
calls.length = 0;
updateVisibleOverlayVisibility({
visibleOverlayVisible: true,
overlayInteractionActive: true,
mainWindow: window as never,
windowTracker: tracker as never,
trackerNotReadyWarningShown: false,
setTrackerNotReadyWarningShown: () => {},
updateVisibleOverlayBounds: () => {
calls.push('update-bounds');
},
ensureOverlayWindowLevel: () => {
calls.push('ensure-level');
},
syncPrimaryOverlayWindowLayer: () => {
calls.push('sync-layer');
},
enforceOverlayLayerOrder: () => {
calls.push('enforce-order');
},
syncOverlayShortcuts: () => {
calls.push('sync-shortcuts');
},
isMacOSPlatform: true,
showOverlayLoadingOsd: () => {
calls.push('loading-osd');
},
} as never);
assert.ok(calls.includes('sync-layer'));
assert.ok(calls.includes('mouse-ignore:false:plain'));
assert.ok(calls.includes('ensure-level'));
assert.ok(calls.includes('enforce-order'));
assert.ok(calls.includes('sync-shortcuts'));
assert.ok(!calls.includes('always-on-top:false'));
assert.ok(!calls.includes('hide'));
assert.ok(!calls.includes('loading-osd'));
});
test('macOS suppresses immediate repeat loading OSD after tracker recovery until cooldown expires', () => { test('macOS suppresses immediate repeat loading OSD after tracker recovery until cooldown expires', () => {
const { window } = createMainWindowRecorder(); const { window } = createMainWindowRecorder();
const osdMessages: string[] = []; const osdMessages: string[] = [];
+17 -2
View File
@@ -63,6 +63,7 @@ export function updateVisibleOverlayVisibility(args: {
visibleOverlayVisible: boolean; visibleOverlayVisible: boolean;
modalActive?: boolean; modalActive?: boolean;
forceMousePassthrough?: boolean; forceMousePassthrough?: boolean;
overlayInteractionActive?: boolean;
mainWindow: BrowserWindow | null; mainWindow: BrowserWindow | null;
windowTracker: BaseWindowTracker | null; windowTracker: BaseWindowTracker | null;
lastKnownWindowsForegroundProcessName?: string | null; lastKnownWindowsForegroundProcessName?: string | null;
@@ -89,6 +90,7 @@ export function updateVisibleOverlayVisibility(args: {
} }
const mainWindow = args.mainWindow; const mainWindow = args.mainWindow;
const overlayInteractionActive = args.overlayInteractionActive === true;
if (args.modalActive) { if (args.modalActive) {
if (args.isWindowsPlatform) { if (args.isWindowsPlatform) {
@@ -104,7 +106,8 @@ export function updateVisibleOverlayVisibility(args: {
const forceMousePassthrough = args.forceMousePassthrough === true; const forceMousePassthrough = args.forceMousePassthrough === true;
const wasVisible = mainWindow.isVisible(); const wasVisible = mainWindow.isVisible();
const isVisibleOverlayFocused = const isVisibleOverlayFocused =
typeof mainWindow.isFocused === 'function' && mainWindow.isFocused(); overlayInteractionActive ||
(typeof mainWindow.isFocused === 'function' && mainWindow.isFocused());
const windowTracker = args.windowTracker; const windowTracker = args.windowTracker;
const canReportMacOSTargetMinimized = const canReportMacOSTargetMinimized =
args.isMacOSPlatform && typeof windowTracker?.isTargetWindowMinimized === 'function'; args.isMacOSPlatform && typeof windowTracker?.isTargetWindowMinimized === 'function';
@@ -130,7 +133,7 @@ export function updateVisibleOverlayVisibility(args: {
!isVisibleOverlayFocused && !isVisibleOverlayFocused &&
!isTrackedMacOSTargetFocused; !isTrackedMacOSTargetFocused;
// Renderer hover tracking temporarily disables this for subtitle and popup interaction. // Renderer hover tracking temporarily disables this for subtitle and popup interaction.
const shouldUseMacOSMousePassthrough = args.isMacOSPlatform; const shouldUseMacOSMousePassthrough = args.isMacOSPlatform && !overlayInteractionActive;
const shouldDefaultToPassthrough = const shouldDefaultToPassthrough =
args.isWindowsPlatform || forceMousePassthrough || shouldReleaseMacOSOverlayLevel; args.isWindowsPlatform || forceMousePassthrough || shouldReleaseMacOSOverlayLevel;
const windowsForegroundProcessName = const windowsForegroundProcessName =
@@ -234,6 +237,16 @@ export function updateVisibleOverlayVisibility(args: {
args.syncWindowsOverlayToMpvZOrder?.(mainWindow); args.syncWindowsOverlayToMpvZOrder?.(mainWindow);
} }
if (
args.isMacOSPlatform &&
overlayInteractionActive &&
!forceMousePassthrough &&
typeof mainWindow.isFocused === 'function' &&
!mainWindow.isFocused()
) {
mainWindow.focus();
}
if (!args.isWindowsPlatform && !args.isMacOSPlatform && !forceMousePassthrough) { if (!args.isWindowsPlatform && !args.isMacOSPlatform && !forceMousePassthrough) {
mainWindow.focus(); mainWindow.focus();
} }
@@ -320,6 +333,7 @@ export function updateVisibleOverlayVisibility(args: {
const hasRetainedTrackedGeometry = args.windowTracker.getGeometry() !== null; const hasRetainedTrackedGeometry = args.windowTracker.getGeometry() !== null;
const hasActiveMacOSTargetSignal = const hasActiveMacOSTargetSignal =
args.isMacOSPlatform && (args.windowTracker.isTargetWindowFocused?.() ?? false); args.isMacOSPlatform && (args.windowTracker.isTargetWindowFocused?.() ?? false);
const hasActiveMacOSOverlaySignal = args.isMacOSPlatform && overlayInteractionActive;
const canReportMacOSTargetMinimized = const canReportMacOSTargetMinimized =
args.isMacOSPlatform && typeof args.windowTracker.isTargetWindowMinimized === 'function'; args.isMacOSPlatform && typeof args.windowTracker.isTargetWindowMinimized === 'function';
const isTrackedMacOSTargetMinimized = const isTrackedMacOSTargetMinimized =
@@ -328,6 +342,7 @@ export function updateVisibleOverlayVisibility(args: {
(args.isMacOSPlatform && (args.isMacOSPlatform &&
!isTrackedMacOSTargetMinimized && !isTrackedMacOSTargetMinimized &&
(hasRetainedTrackedGeometry || (hasRetainedTrackedGeometry ||
(mainWindow.isVisible() && hasActiveMacOSOverlaySignal) ||
(mainWindow.isVisible() && hasActiveMacOSTargetSignal) || (mainWindow.isVisible() && hasActiveMacOSTargetSignal) ||
(canReportMacOSTargetMinimized && mainWindow.isVisible()))) || (canReportMacOSTargetMinimized && mainWindow.isVisible()))) ||
(args.isWindowsPlatform && (args.isWindowsPlatform &&
+27
View File
@@ -2069,6 +2069,7 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
getModalActive: () => overlayModalInputState.getModalInputExclusive(), getModalActive: () => overlayModalInputState.getModalInputExclusive(),
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getForceMousePassthrough: () => appState.statsOverlayVisible, getForceMousePassthrough: () => appState.statsOverlayVisible,
getOverlayInteractionActive: () => visibleOverlayInteractionActive,
getWindowTracker: () => appState.windowTracker, getWindowTracker: () => appState.windowTracker,
getLastKnownWindowsForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName, getLastKnownWindowsForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName,
getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(), getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
@@ -2123,6 +2124,7 @@ let windowsVisibleOverlayZOrderSyncQueued = false;
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null; let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null; let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
let lastWindowsVisibleOverlayBlurredAtMs = 0; let lastWindowsVisibleOverlayBlurredAtMs = 0;
let visibleOverlayInteractionActive = false;
function clearVisibleOverlayBlurRefreshTimeouts(): void { function clearVisibleOverlayBlurRefreshTimeouts(): void {
for (const timeout of visibleOverlayBlurRefreshTimeouts) { for (const timeout of visibleOverlayBlurRefreshTimeouts) {
@@ -3045,6 +3047,7 @@ const {
resetAnilistMediaTracking, resetAnilistMediaTracking,
getAnilistMediaGuessRuntimeState, getAnilistMediaGuessRuntimeState,
setAnilistMediaGuessRuntimeState, setAnilistMediaGuessRuntimeState,
recordAnilistMediaDuration,
resetAnilistMediaGuessState, resetAnilistMediaGuessState,
maybeProbeAnilistDuration, maybeProbeAnilistDuration,
ensureAnilistMediaGuess, ensureAnilistMediaGuess,
@@ -3148,6 +3151,13 @@ const {
); );
}, },
}, },
recordMediaDurationMainDeps: {
getCurrentMediaKey: () => getCurrentAnilistMediaKey(),
getState: () => getAnilistMediaGuessRuntimeState(),
setState: (state) => {
setAnilistMediaGuessRuntimeState(state);
},
},
resetMediaGuessStateMainDeps: { resetMediaGuessStateMainDeps: {
setMediaGuess: (value) => { setMediaGuess: (value) => {
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState( anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
@@ -3987,6 +3997,9 @@ const {
void reportJellyfinRemoteStopped(); void reportJellyfinRemoteStopped();
}, },
maybeRunAnilistPostWatchUpdate: (options) => maybeRunAnilistPostWatchUpdate(options), maybeRunAnilistPostWatchUpdate: (options) => maybeRunAnilistPostWatchUpdate(options),
recordAnilistMediaDuration: (durationSec) => {
recordAnilistMediaDuration(durationSec);
},
logSubtitleTimingError: (message, error) => logger.error(message, error), logSubtitleTimingError: (message, error) => logger.error(message, error),
broadcastToOverlayWindows: (channel, payload) => { broadcastToOverlayWindows: (channel, payload) => {
broadcastToOverlayWindows(channel, payload); broadcastToOverlayWindows(channel, payload);
@@ -5128,6 +5141,20 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
onOverlayModalOpened: (modal) => { onOverlayModalOpened: (modal) => {
overlayModalRuntime.notifyOverlayModalOpened(modal); overlayModalRuntime.notifyOverlayModalOpened(modal);
}, },
onOverlayMouseInteractionChanged: (active, senderWindow) => {
const mainWindow = overlayManager.getMainWindow();
if (!mainWindow || senderWindow !== mainWindow) {
return;
}
if (visibleOverlayInteractionActive === active) {
if (active && process.platform === 'darwin' && !mainWindow.isFocused()) {
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
}
return;
}
visibleOverlayInteractionActive = active;
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
},
onYoutubePickerResolve: (request) => youtubeFlowRuntime.resolveActivePicker(request), onYoutubePickerResolve: (request) => youtubeFlowRuntime.resolveActivePicker(request),
openYomitanSettings: () => openYomitanSettings(), openYomitanSettings: () => openYomitanSettings(),
quitApp: () => requestAppQuit(), quitApp: () => requestAppQuit(),
+2
View File
@@ -57,6 +57,7 @@ export interface MainIpcRuntimeServiceDepsParams {
getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility']; getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility'];
onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed']; onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed'];
onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened']; onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened'];
onOverlayMouseInteractionChanged?: IpcDepsRuntimeOptions['onOverlayMouseInteractionChanged'];
onYoutubePickerResolve: IpcDepsRuntimeOptions['onYoutubePickerResolve']; onYoutubePickerResolve: IpcDepsRuntimeOptions['onYoutubePickerResolve'];
openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings']; openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings'];
quitApp: IpcDepsRuntimeOptions['quitApp']; quitApp: IpcDepsRuntimeOptions['quitApp'];
@@ -229,6 +230,7 @@ export function createMainIpcRuntimeServiceDeps(
getVisibleOverlayVisibility: params.getVisibleOverlayVisibility, getVisibleOverlayVisibility: params.getVisibleOverlayVisibility,
onOverlayModalClosed: params.onOverlayModalClosed, onOverlayModalClosed: params.onOverlayModalClosed,
onOverlayModalOpened: params.onOverlayModalOpened, onOverlayModalOpened: params.onOverlayModalOpened,
onOverlayMouseInteractionChanged: params.onOverlayMouseInteractionChanged,
onYoutubePickerResolve: params.onYoutubePickerResolve, onYoutubePickerResolve: params.onYoutubePickerResolve,
openYomitanSettings: params.openYomitanSettings, openYomitanSettings: params.openYomitanSettings,
quitApp: params.quitApp, quitApp: params.quitApp,
+2
View File
@@ -11,6 +11,7 @@ export interface OverlayVisibilityRuntimeDeps {
getModalActive: () => boolean; getModalActive: () => boolean;
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
getForceMousePassthrough: () => boolean; getForceMousePassthrough: () => boolean;
getOverlayInteractionActive?: () => boolean;
getWindowTracker: () => BaseWindowTracker | null; getWindowTracker: () => BaseWindowTracker | null;
getLastKnownWindowsForegroundProcessName?: () => string | null; getLastKnownWindowsForegroundProcessName?: () => string | null;
getWindowsOverlayProcessName?: () => string | null; getWindowsOverlayProcessName?: () => string | null;
@@ -49,6 +50,7 @@ export function createOverlayVisibilityRuntimeService(
visibleOverlayVisible, visibleOverlayVisible,
modalActive: deps.getModalActive(), modalActive: deps.getModalActive(),
forceMousePassthrough, forceMousePassthrough,
overlayInteractionActive: deps.getOverlayInteractionActive?.() ?? false,
mainWindow, mainWindow,
windowTracker, windowTracker,
lastKnownWindowsForegroundProcessName: deps.getLastKnownWindowsForegroundProcessName?.(), lastKnownWindowsForegroundProcessName: deps.getLastKnownWindowsForegroundProcessName?.(),
@@ -3,6 +3,7 @@ import test from 'node:test';
import { import {
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler, createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler,
createBuildGetCurrentAnilistMediaKeyMainDepsHandler, createBuildGetCurrentAnilistMediaKeyMainDepsHandler,
createBuildRecordAnilistMediaDurationMainDepsHandler,
createBuildResetAnilistMediaGuessStateMainDepsHandler, createBuildResetAnilistMediaGuessStateMainDepsHandler,
createBuildResetAnilistMediaTrackingMainDepsHandler, createBuildResetAnilistMediaTrackingMainDepsHandler,
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler, createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler,
@@ -70,3 +71,32 @@ test('reset anilist media guess state main deps builder maps callbacks', () => {
deps.setMediaGuessPromise(null); deps.setMediaGuessPromise(null);
assert.deepEqual(calls, ['guess', 'promise']); assert.deepEqual(calls, ['guess', 'promise']);
}); });
test('record anilist media duration main deps builder maps callbacks', () => {
const calls: string[] = [];
const state = {
mediaKey: '/tmp/video.mkv',
mediaDurationSec: null,
mediaGuess: null,
mediaGuessPromise: null,
lastDurationProbeAtMs: 0,
};
const deps = createBuildRecordAnilistMediaDurationMainDepsHandler({
getCurrentMediaKey: () => {
calls.push('key');
return '/tmp/video.mkv';
},
getState: () => {
calls.push('get');
return state;
},
setState: () => {
calls.push('set');
},
})();
assert.equal(deps.getCurrentMediaKey(), '/tmp/video.mkv');
deps.getState();
deps.setState(state);
assert.deepEqual(calls, ['key', 'get', 'set']);
});
@@ -1,6 +1,7 @@
import type { import type {
createGetAnilistMediaGuessRuntimeStateHandler, createGetAnilistMediaGuessRuntimeStateHandler,
createGetCurrentAnilistMediaKeyHandler, createGetCurrentAnilistMediaKeyHandler,
createRecordAnilistMediaDurationHandler,
createResetAnilistMediaGuessStateHandler, createResetAnilistMediaGuessStateHandler,
createResetAnilistMediaTrackingHandler, createResetAnilistMediaTrackingHandler,
createSetAnilistMediaGuessRuntimeStateHandler, createSetAnilistMediaGuessRuntimeStateHandler,
@@ -18,6 +19,9 @@ type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters<
type SetAnilistMediaGuessRuntimeStateMainDeps = Parameters< type SetAnilistMediaGuessRuntimeStateMainDeps = Parameters<
typeof createSetAnilistMediaGuessRuntimeStateHandler typeof createSetAnilistMediaGuessRuntimeStateHandler
>[0]; >[0];
type RecordAnilistMediaDurationMainDeps = Parameters<
typeof createRecordAnilistMediaDurationHandler
>[0];
type ResetAnilistMediaGuessStateMainDeps = Parameters< type ResetAnilistMediaGuessStateMainDeps = Parameters<
typeof createResetAnilistMediaGuessStateHandler typeof createResetAnilistMediaGuessStateHandler
>[0]; >[0];
@@ -66,6 +70,16 @@ export function createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler(
}); });
} }
export function createBuildRecordAnilistMediaDurationMainDepsHandler(
deps: RecordAnilistMediaDurationMainDeps,
) {
return (): RecordAnilistMediaDurationMainDeps => ({
getCurrentMediaKey: () => deps.getCurrentMediaKey(),
getState: () => deps.getState(),
setState: (state) => deps.setState(state),
});
}
export function createBuildResetAnilistMediaGuessStateMainDepsHandler( export function createBuildResetAnilistMediaGuessStateMainDepsHandler(
deps: ResetAnilistMediaGuessStateMainDeps, deps: ResetAnilistMediaGuessStateMainDeps,
) { ) {
@@ -3,6 +3,7 @@ import test from 'node:test';
import { import {
createGetAnilistMediaGuessRuntimeStateHandler, createGetAnilistMediaGuessRuntimeStateHandler,
createGetCurrentAnilistMediaKeyHandler, createGetCurrentAnilistMediaKeyHandler,
createRecordAnilistMediaDurationHandler,
createResetAnilistMediaGuessStateHandler, createResetAnilistMediaGuessStateHandler,
createResetAnilistMediaTrackingHandler, createResetAnilistMediaTrackingHandler,
createSetAnilistMediaGuessRuntimeStateHandler, createSetAnilistMediaGuessRuntimeStateHandler,
@@ -176,3 +177,57 @@ test('reset anilist media guess state clears guess and in-flight promise', () =>
assert.equal(state.mediaDurationSec, 240); assert.equal(state.mediaDurationSec, 240);
assert.equal(state.lastDurationProbeAtMs, 321); assert.equal(state.lastDurationProbeAtMs, 321);
}); });
test('record anilist media duration stores observed mpv duration for current media', () => {
const existingPromise = Promise.resolve(null);
let state = {
mediaKey: '/tmp/video.mkv' as string | null,
mediaDurationSec: null as number | null,
mediaGuess: { title: 'guess' } as { title: string } | null,
mediaGuessPromise: existingPromise as Promise<unknown> | null,
lastDurationProbeAtMs: 321,
};
const recordDuration = createRecordAnilistMediaDurationHandler({
getCurrentMediaKey: () => '/tmp/video.mkv',
getState: () => state as never,
setState: (nextState) => {
state = nextState as never;
},
});
recordDuration(1440);
assert.equal(state.mediaDurationSec, 1440);
assert.deepEqual(state.mediaGuess, { title: 'guess' });
assert.equal(state.mediaGuessPromise, existingPromise);
assert.equal(state.lastDurationProbeAtMs, 321);
});
test('record anilist media duration resets stale media state when media key changes', () => {
let state = {
mediaKey: '/tmp/old.mkv' as string | null,
mediaDurationSec: 120 as number | null,
mediaGuess: { title: 'old' } as { title: string } | null,
mediaGuessPromise: Promise.resolve(null) as Promise<unknown> | null,
lastDurationProbeAtMs: 321,
};
const recordDuration = createRecordAnilistMediaDurationHandler({
getCurrentMediaKey: () => '/tmp/new.mkv',
getState: () => state as never,
setState: (nextState) => {
state = nextState as never;
},
});
recordDuration(1440);
assert.deepEqual(state, {
mediaKey: '/tmp/new.mkv',
mediaDurationSec: 1440,
mediaGuess: null,
mediaGuessPromise: null,
lastDurationProbeAtMs: 0,
});
});
+31
View File
@@ -61,6 +61,37 @@ export function createSetAnilistMediaGuessRuntimeStateHandler(deps: {
}; };
} }
export function createRecordAnilistMediaDurationHandler(deps: {
getCurrentMediaKey: () => string | null;
getState: () => AnilistMediaGuessRuntimeState;
setState: (state: AnilistMediaGuessRuntimeState) => void;
}) {
return (durationSec: number): void => {
if (!Number.isFinite(durationSec) || durationSec <= 0) {
return;
}
const mediaKey = deps.getCurrentMediaKey();
if (!mediaKey) {
return;
}
const state = deps.getState();
if (state.mediaKey === mediaKey) {
deps.setState({
...state,
mediaDurationSec: durationSec,
});
return;
}
deps.setState({
mediaKey,
mediaDurationSec: durationSec,
mediaGuess: null,
mediaGuessPromise: null,
lastDurationProbeAtMs: 0,
});
};
}
export function createResetAnilistMediaGuessStateHandler(deps: { export function createResetAnilistMediaGuessStateHandler(deps: {
setMediaGuess: (value: AnilistMediaGuessRuntimeState['mediaGuess']) => void; setMediaGuess: (value: AnilistMediaGuessRuntimeState['mediaGuess']) => void;
setMediaGuessPromise: (value: AnilistMediaGuessRuntimeState['mediaGuessPromise']) => void; setMediaGuessPromise: (value: AnilistMediaGuessRuntimeState['mediaGuessPromise']) => void;
@@ -80,6 +80,23 @@ test('composeAnilistTrackingHandlers returns callable handlers and forwards call
lastDurationProbeAtMsState = value; lastDurationProbeAtMsState = value;
}, },
}, },
recordMediaDurationMainDeps: {
getCurrentMediaKey: () => 'media-key',
getState: () => ({
mediaKey: mediaKeyState,
mediaDurationSec: mediaDurationSecState,
mediaGuess: mediaGuessState,
mediaGuessPromise: mediaGuessPromiseState,
lastDurationProbeAtMs: lastDurationProbeAtMsState,
}),
setState: (state) => {
mediaKeyState = state.mediaKey;
mediaDurationSecState = state.mediaDurationSec;
mediaGuessState = state.mediaGuess;
mediaGuessPromiseState = state.mediaGuessPromise;
lastDurationProbeAtMsState = state.lastDurationProbeAtMs;
},
},
resetMediaGuessStateMainDeps: { resetMediaGuessStateMainDeps: {
setMediaGuess: (value) => { setMediaGuess: (value) => {
mediaGuessState = value; mediaGuessState = value;
@@ -192,6 +209,7 @@ test('composeAnilistTrackingHandlers returns callable handlers and forwards call
assert.equal(typeof composed.resetAnilistMediaTracking, 'function'); assert.equal(typeof composed.resetAnilistMediaTracking, 'function');
assert.equal(typeof composed.getAnilistMediaGuessRuntimeState, 'function'); assert.equal(typeof composed.getAnilistMediaGuessRuntimeState, 'function');
assert.equal(typeof composed.setAnilistMediaGuessRuntimeState, 'function'); assert.equal(typeof composed.setAnilistMediaGuessRuntimeState, 'function');
assert.equal(typeof composed.recordAnilistMediaDuration, 'function');
assert.equal(typeof composed.resetAnilistMediaGuessState, 'function'); assert.equal(typeof composed.resetAnilistMediaGuessState, 'function');
assert.equal(typeof composed.maybeProbeAnilistDuration, 'function'); assert.equal(typeof composed.maybeProbeAnilistDuration, 'function');
assert.equal(typeof composed.ensureAnilistMediaGuess, 'function'); assert.equal(typeof composed.ensureAnilistMediaGuess, 'function');
@@ -216,6 +234,9 @@ test('composeAnilistTrackingHandlers returns callable handlers and forwards call
}); });
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaDurationSec, 90); assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaDurationSec, 90);
composed.recordAnilistMediaDuration(180);
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaDurationSec, 180);
composed.resetAnilistMediaGuessState(); composed.resetAnilistMediaGuessState();
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaGuess, null); assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaGuess, null);
@@ -5,6 +5,7 @@ import {
createBuildMaybeProbeAnilistDurationMainDepsHandler, createBuildMaybeProbeAnilistDurationMainDepsHandler,
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler, createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler,
createBuildProcessNextAnilistRetryUpdateMainDepsHandler, createBuildProcessNextAnilistRetryUpdateMainDepsHandler,
createBuildRecordAnilistMediaDurationMainDepsHandler,
createBuildRefreshAnilistClientSecretStateMainDepsHandler, createBuildRefreshAnilistClientSecretStateMainDepsHandler,
createBuildResetAnilistMediaGuessStateMainDepsHandler, createBuildResetAnilistMediaGuessStateMainDepsHandler,
createBuildResetAnilistMediaTrackingMainDepsHandler, createBuildResetAnilistMediaTrackingMainDepsHandler,
@@ -15,6 +16,7 @@ import {
createMaybeProbeAnilistDurationHandler, createMaybeProbeAnilistDurationHandler,
createMaybeRunAnilistPostWatchUpdateHandler, createMaybeRunAnilistPostWatchUpdateHandler,
createProcessNextAnilistRetryUpdateHandler, createProcessNextAnilistRetryUpdateHandler,
createRecordAnilistMediaDurationHandler,
createRefreshAnilistClientSecretStateHandler, createRefreshAnilistClientSecretStateHandler,
createResetAnilistMediaGuessStateHandler, createResetAnilistMediaGuessStateHandler,
createResetAnilistMediaTrackingHandler, createResetAnilistMediaTrackingHandler,
@@ -38,6 +40,9 @@ export type AnilistTrackingComposerOptions = ComposerInputs<{
setMediaGuessRuntimeStateMainDeps: Parameters< setMediaGuessRuntimeStateMainDeps: Parameters<
typeof createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler typeof createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler
>[0]; >[0];
recordMediaDurationMainDeps: Parameters<
typeof createBuildRecordAnilistMediaDurationMainDepsHandler
>[0];
resetMediaGuessStateMainDeps: Parameters< resetMediaGuessStateMainDeps: Parameters<
typeof createBuildResetAnilistMediaGuessStateMainDepsHandler typeof createBuildResetAnilistMediaGuessStateMainDepsHandler
>[0]; >[0];
@@ -63,6 +68,7 @@ export type AnilistTrackingComposerResult = ComposerOutputs<{
setAnilistMediaGuessRuntimeState: ReturnType< setAnilistMediaGuessRuntimeState: ReturnType<
typeof createSetAnilistMediaGuessRuntimeStateHandler typeof createSetAnilistMediaGuessRuntimeStateHandler
>; >;
recordAnilistMediaDuration: ReturnType<typeof createRecordAnilistMediaDurationHandler>;
resetAnilistMediaGuessState: ReturnType<typeof createResetAnilistMediaGuessStateHandler>; resetAnilistMediaGuessState: ReturnType<typeof createResetAnilistMediaGuessStateHandler>;
maybeProbeAnilistDuration: ReturnType<typeof createMaybeProbeAnilistDurationHandler>; maybeProbeAnilistDuration: ReturnType<typeof createMaybeProbeAnilistDurationHandler>;
ensureAnilistMediaGuess: ReturnType<typeof createEnsureAnilistMediaGuessHandler>; ensureAnilistMediaGuess: ReturnType<typeof createEnsureAnilistMediaGuessHandler>;
@@ -94,6 +100,9 @@ export function composeAnilistTrackingHandlers(
options.setMediaGuessRuntimeStateMainDeps, options.setMediaGuessRuntimeStateMainDeps,
)(), )(),
); );
const recordAnilistMediaDuration = createRecordAnilistMediaDurationHandler(
createBuildRecordAnilistMediaDurationMainDepsHandler(options.recordMediaDurationMainDeps)(),
);
const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler( const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler(
createBuildResetAnilistMediaGuessStateMainDepsHandler(options.resetMediaGuessStateMainDeps)(), createBuildResetAnilistMediaGuessStateMainDepsHandler(options.resetMediaGuessStateMainDeps)(),
); );
@@ -120,6 +129,7 @@ export function composeAnilistTrackingHandlers(
resetAnilistMediaTracking, resetAnilistMediaTracking,
getAnilistMediaGuessRuntimeState, getAnilistMediaGuessRuntimeState,
setAnilistMediaGuessRuntimeState, setAnilistMediaGuessRuntimeState,
recordAnilistMediaDuration,
resetAnilistMediaGuessState, resetAnilistMediaGuessState,
maybeProbeAnilistDuration, maybeProbeAnilistDuration,
ensureAnilistMediaGuess, ensureAnilistMediaGuess,
@@ -16,6 +16,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
recordSubtitleLine: (text: string) => calls.push(`immersion-sub:${text}`), recordSubtitleLine: (text: string) => calls.push(`immersion-sub:${text}`),
handleMediaTitleUpdate: (title: string) => calls.push(`immersion-title:${title}`), handleMediaTitleUpdate: (title: string) => calls.push(`immersion-title:${title}`),
recordPlaybackPosition: (time: number) => calls.push(`immersion-time:${time}`), recordPlaybackPosition: (time: number) => calls.push(`immersion-time:${time}`),
recordMediaDuration: (durationSec: number) => calls.push(`immersion-duration:${durationSec}`),
recordPauseState: (paused: boolean) => calls.push(`immersion-pause:${paused}`), recordPauseState: (paused: boolean) => calls.push(`immersion-pause:${paused}`),
}, },
subtitleTimingTracker: { subtitleTimingTracker: {
@@ -40,6 +41,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
maybeRunAnilistPostWatchUpdate: async () => { maybeRunAnilistPostWatchUpdate: async () => {
calls.push('anilist-post-watch'); calls.push('anilist-post-watch');
}, },
recordAnilistMediaDuration: (durationSec) => calls.push(`anilist-duration:${durationSec}`),
logSubtitleTimingError: (message) => calls.push(`subtitle-error:${message}`), logSubtitleTimingError: (message) => calls.push(`subtitle-error:${message}`),
broadcastToOverlayWindows: (channel, payload) => broadcastToOverlayWindows: (channel, payload) =>
calls.push(`broadcast:${channel}:${String(payload)}`), calls.push(`broadcast:${channel}:${String(payload)}`),
@@ -95,6 +97,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
deps.resetAnilistMediaGuessState(); deps.resetAnilistMediaGuessState();
deps.notifyImmersionTitleUpdate('title'); deps.notifyImmersionTitleUpdate('title');
deps.recordPlaybackPosition(10); deps.recordPlaybackPosition(10);
deps.recordMediaDuration(1234);
deps.reportJellyfinRemoteProgress(true); deps.reportJellyfinRemoteProgress(true);
deps.onFullscreenChange?.(true); deps.onFullscreenChange?.(true);
deps.recordPauseState(true); deps.recordPauseState(true);
@@ -118,6 +121,8 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
assert.ok(calls.includes('presence-refresh')); assert.ok(calls.includes('presence-refresh'));
assert.ok(calls.includes('restore-mpv-sub')); assert.ok(calls.includes('restore-mpv-sub'));
assert.ok(calls.includes('reset-sidebar-layout')); assert.ok(calls.includes('reset-sidebar-layout'));
assert.ok(calls.includes('immersion-duration:1234'));
assert.ok(calls.includes('anilist-duration:1234'));
}); });
test('mpv main event main deps wire subtitle callbacks without suppression gate', () => { test('mpv main event main deps wire subtitle callbacks without suppression gate', () => {
@@ -47,6 +47,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
reportJellyfinRemoteStopped: () => void; reportJellyfinRemoteStopped: () => void;
syncOverlayMpvSubtitleSuppression: () => void; syncOverlayMpvSubtitleSuppression: () => void;
maybeRunAnilistPostWatchUpdate: (options?: AnilistPostWatchRunOptions) => Promise<void>; maybeRunAnilistPostWatchUpdate: (options?: AnilistPostWatchRunOptions) => Promise<void>;
recordAnilistMediaDuration?: (durationSec: number) => void;
logSubtitleTimingError: (message: string, error: unknown) => void; logSubtitleTimingError: (message: string, error: unknown) => void;
broadcastToOverlayWindows: (channel: string, payload: unknown) => void; broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
getImmediateSubtitlePayload?: (text: string) => SubtitleData | null; getImmediateSubtitlePayload?: (text: string) => SubtitleData | null;
@@ -184,6 +185,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
recordMediaDuration: (durationSec: number) => { recordMediaDuration: (durationSec: number) => {
deps.ensureImmersionTrackerInitialized(); deps.ensureImmersionTrackerInitialized();
deps.appState.immersionTracker?.recordMediaDuration?.(durationSec); deps.appState.immersionTracker?.recordMediaDuration?.(durationSec);
deps.recordAnilistMediaDuration?.(durationSec);
}, },
reportJellyfinRemoteProgress: (forceImmediate: boolean) => reportJellyfinRemoteProgress: (forceImmediate: boolean) =>
deps.reportJellyfinRemoteProgress(forceImmediate), deps.reportJellyfinRemoteProgress(forceImmediate),
@@ -15,6 +15,7 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
getModalActive: () => true, getModalActive: () => true,
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getForceMousePassthrough: () => true, getForceMousePassthrough: () => true,
getOverlayInteractionActive: () => true,
getWindowTracker: () => tracker, getWindowTracker: () => tracker,
getLastKnownWindowsForegroundProcessName: () => 'mpv', getLastKnownWindowsForegroundProcessName: () => 'mpv',
getWindowsOverlayProcessName: () => 'subminer', getWindowsOverlayProcessName: () => 'subminer',
@@ -40,6 +41,7 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
assert.equal(deps.getModalActive(), true); assert.equal(deps.getModalActive(), true);
assert.equal(deps.getVisibleOverlayVisible(), true); assert.equal(deps.getVisibleOverlayVisible(), true);
assert.equal(deps.getForceMousePassthrough(), true); assert.equal(deps.getForceMousePassthrough(), true);
assert.equal(deps.getOverlayInteractionActive?.(), true);
assert.equal(deps.getLastKnownWindowsForegroundProcessName?.(), 'mpv'); assert.equal(deps.getLastKnownWindowsForegroundProcessName?.(), 'mpv');
assert.equal(deps.getWindowsOverlayProcessName?.(), 'subminer'); assert.equal(deps.getWindowsOverlayProcessName?.(), 'subminer');
assert.equal(deps.getWindowsFocusHandoffGraceActive?.(), true); assert.equal(deps.getWindowsFocusHandoffGraceActive?.(), true);
@@ -10,6 +10,7 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
getModalActive: () => deps.getModalActive(), getModalActive: () => deps.getModalActive(),
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
getForceMousePassthrough: () => deps.getForceMousePassthrough(), getForceMousePassthrough: () => deps.getForceMousePassthrough(),
getOverlayInteractionActive: () => deps.getOverlayInteractionActive?.() ?? false,
getWindowTracker: () => deps.getWindowTracker(), getWindowTracker: () => deps.getWindowTracker(),
getLastKnownWindowsForegroundProcessName: () => getLastKnownWindowsForegroundProcessName: () =>
deps.getLastKnownWindowsForegroundProcessName?.() ?? null, deps.getLastKnownWindowsForegroundProcessName?.() ?? null,