mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user