mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
fix(overlay): stabilize macOS focus handoff and sidebar Yomitan pause
- Keep overlay visible during macOS foreground probe after overlay blur - Hold sidebar hover-pause while a Yomitan lookup popup remains open
This commit is contained in:
@@ -1260,6 +1260,54 @@ test('macOS tracked overlay hides when mpv loses foreground', () => {
|
||||
assert.ok(!calls.includes('show'));
|
||||
});
|
||||
|
||||
test('macOS keeps visible overlay stable while probing frontmost app after overlay blur', () => {
|
||||
const { window, calls } = createMainWindowRecorder();
|
||||
const tracker: WindowTrackerStub = {
|
||||
isTracking: () => true,
|
||||
getGeometry: () => ({ x: 0, y: 0, width: 1280, height: 720 }),
|
||||
isTargetWindowFocused: () => false,
|
||||
isTargetWindowMinimized: () => false,
|
||||
};
|
||||
|
||||
window.show();
|
||||
calls.length = 0;
|
||||
|
||||
updateVisibleOverlayVisibility({
|
||||
visibleOverlayVisible: 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,
|
||||
macOSForegroundProbeActive: true,
|
||||
} as never);
|
||||
|
||||
assert.ok(calls.includes('update-bounds'));
|
||||
assert.ok(calls.includes('sync-layer'));
|
||||
assert.ok(calls.includes('mouse-ignore:true:forward'));
|
||||
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 keeps tracked overlay visible while overlay interaction is active after mpv loses foreground', () => {
|
||||
const { window, calls, setFocused } = createMainWindowRecorder();
|
||||
const tracker: WindowTrackerStub = {
|
||||
|
||||
@@ -71,6 +71,7 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
lastKnownWindowsForegroundProcessName?: string | null;
|
||||
windowsOverlayProcessName?: string | null;
|
||||
windowsFocusHandoffGraceActive?: boolean;
|
||||
macOSForegroundProbeActive?: boolean;
|
||||
trackerNotReadyWarningShown: boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
@@ -128,6 +129,12 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
const isTrackedMacOSTargetMinimized =
|
||||
canReportMacOSTargetMinimized && windowTracker?.isTargetWindowMinimized() === true;
|
||||
const trackedMacOSTargetFocused = args.windowTracker?.isTargetWindowFocused?.();
|
||||
const shouldPreserveMacOSOverlayDuringForegroundProbe =
|
||||
args.isMacOSPlatform &&
|
||||
args.macOSForegroundProbeActive === true &&
|
||||
!!windowTracker &&
|
||||
!isTrackedMacOSTargetMinimized &&
|
||||
(windowTracker.isTracking() || windowTracker.getGeometry() !== null);
|
||||
const hasTransientMacOSTrackerLoss =
|
||||
args.isMacOSPlatform &&
|
||||
canReportMacOSTargetMinimized &&
|
||||
@@ -137,7 +144,10 @@ export function updateVisibleOverlayVisibility(args: {
|
||||
trackedMacOSTargetFocused !== false &&
|
||||
mainWindow.isVisible();
|
||||
const isTrackedMacOSTargetFocused =
|
||||
hasTransientMacOSTrackerLoss || !args.isMacOSPlatform || !args.windowTracker
|
||||
hasTransientMacOSTrackerLoss ||
|
||||
shouldPreserveMacOSOverlayDuringForegroundProbe ||
|
||||
!args.isMacOSPlatform ||
|
||||
!args.windowTracker
|
||||
? true
|
||||
: (trackedMacOSTargetFocused ?? true);
|
||||
const shouldReleaseMacOSOverlayLevel =
|
||||
|
||||
+49
@@ -2280,6 +2280,7 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
||||
getLastKnownWindowsForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName,
|
||||
getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(),
|
||||
getWindowsFocusHandoffGraceActive: () => hasWindowsVisibleOverlayFocusHandoffGrace(),
|
||||
getMacOSForegroundProbeActive: () => macOSVisibleOverlayForegroundProbeActive,
|
||||
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
||||
appState.trackerNotReadyWarningShown = shown;
|
||||
@@ -2323,6 +2324,7 @@ const VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as const;
|
||||
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
|
||||
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
|
||||
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
|
||||
const MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS = 1_200;
|
||||
let visibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||
let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = [];
|
||||
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
||||
@@ -2331,6 +2333,9 @@ let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval>
|
||||
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
||||
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
||||
let visibleOverlayInteractionActive = false;
|
||||
let macOSVisibleOverlayForegroundProbeActive = false;
|
||||
let macOSVisibleOverlayForegroundProbeToken = 0;
|
||||
let macOSVisibleOverlayForegroundProbeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const handleStatsOverlayVisibilityChanged = createStatsOverlayVisibilityChangeHandler({
|
||||
setStatsOverlayVisibleState: (visible) => {
|
||||
@@ -2357,6 +2362,49 @@ function clearWindowsVisibleOverlayZOrderRetryTimeouts(): void {
|
||||
windowsVisibleOverlayZOrderRetryTimeouts = [];
|
||||
}
|
||||
|
||||
function finishMacOSVisibleOverlayForegroundProbe(token: number): void {
|
||||
if (token !== macOSVisibleOverlayForegroundProbeToken) {
|
||||
return;
|
||||
}
|
||||
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
||||
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
||||
macOSVisibleOverlayForegroundProbeTimeout = null;
|
||||
}
|
||||
if (!macOSVisibleOverlayForegroundProbeActive) {
|
||||
return;
|
||||
}
|
||||
macOSVisibleOverlayForegroundProbeActive = false;
|
||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||
}
|
||||
|
||||
function startMacOSVisibleOverlayForegroundProbe(): void {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
const tracker = appState.windowTracker;
|
||||
if (!tracker) {
|
||||
return;
|
||||
}
|
||||
|
||||
macOSVisibleOverlayForegroundProbeActive = true;
|
||||
const token = ++macOSVisibleOverlayForegroundProbeToken;
|
||||
if (macOSVisibleOverlayForegroundProbeTimeout !== null) {
|
||||
clearTimeout(macOSVisibleOverlayForegroundProbeTimeout);
|
||||
}
|
||||
macOSVisibleOverlayForegroundProbeTimeout = setTimeout(() => {
|
||||
finishMacOSVisibleOverlayForegroundProbe(token);
|
||||
}, MACOS_VISIBLE_OVERLAY_FOREGROUND_PROBE_TIMEOUT_MS);
|
||||
|
||||
void tracker
|
||||
.refreshNow()
|
||||
.catch((error) => {
|
||||
logger.warn('Failed to refresh macOS frontmost app after overlay blur', error);
|
||||
})
|
||||
.finally(() => {
|
||||
finishMacOSVisibleOverlayForegroundProbe(token);
|
||||
});
|
||||
}
|
||||
|
||||
function getWindowsNativeWindowHandle(window: BrowserWindow): string {
|
||||
const handle = window.getNativeWindowHandle();
|
||||
return handle.length >= 8
|
||||
@@ -2555,6 +2603,7 @@ function scheduleVisibleOverlayBlurRefresh(): void {
|
||||
if (process.platform === 'win32') {
|
||||
lastWindowsVisibleOverlayBlurredAtMs = Date.now();
|
||||
}
|
||||
startMacOSVisibleOverlayForegroundProbe();
|
||||
clearVisibleOverlayBlurRefreshTimeouts();
|
||||
for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) {
|
||||
const refreshTimeout = setTimeout(() => {
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface OverlayVisibilityRuntimeDeps {
|
||||
getLastKnownWindowsForegroundProcessName?: () => string | null;
|
||||
getWindowsOverlayProcessName?: () => string | null;
|
||||
getWindowsFocusHandoffGraceActive?: () => boolean;
|
||||
getMacOSForegroundProbeActive?: () => boolean;
|
||||
getTrackerNotReadyWarningShown: () => boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
@@ -59,6 +60,7 @@ export function createOverlayVisibilityRuntimeService(
|
||||
lastKnownWindowsForegroundProcessName: deps.getLastKnownWindowsForegroundProcessName?.(),
|
||||
windowsOverlayProcessName: deps.getWindowsOverlayProcessName?.() ?? null,
|
||||
windowsFocusHandoffGraceActive: deps.getWindowsFocusHandoffGraceActive?.() ?? false,
|
||||
macOSForegroundProbeActive: deps.getMacOSForegroundProbeActive?.() ?? false,
|
||||
trackerNotReadyWarningShown: deps.getTrackerNotReadyWarningShown(),
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
||||
deps.setTrackerNotReadyWarningShown(shown);
|
||||
|
||||
@@ -21,6 +21,7 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
|
||||
getLastKnownWindowsForegroundProcessName: () => 'mpv',
|
||||
getWindowsOverlayProcessName: () => 'subminer',
|
||||
getWindowsFocusHandoffGraceActive: () => true,
|
||||
getMacOSForegroundProbeActive: () => true,
|
||||
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
|
||||
setTrackerNotReadyWarningShown: (shown) => {
|
||||
trackerNotReadyWarningShown = shown;
|
||||
@@ -47,6 +48,7 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
|
||||
assert.equal(deps.getLastKnownWindowsForegroundProcessName?.(), 'mpv');
|
||||
assert.equal(deps.getWindowsOverlayProcessName?.(), 'subminer');
|
||||
assert.equal(deps.getWindowsFocusHandoffGraceActive?.(), true);
|
||||
assert.equal(deps.getMacOSForegroundProbeActive?.(), true);
|
||||
assert.equal(deps.getTrackerNotReadyWarningShown(), false);
|
||||
deps.setTrackerNotReadyWarningShown(true);
|
||||
deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
|
||||
|
||||
@@ -17,6 +17,7 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
|
||||
deps.getLastKnownWindowsForegroundProcessName?.() ?? null,
|
||||
getWindowsOverlayProcessName: () => deps.getWindowsOverlayProcessName?.() ?? null,
|
||||
getWindowsFocusHandoffGraceActive: () => deps.getWindowsFocusHandoffGraceActive?.() ?? false,
|
||||
getMacOSForegroundProbeActive: () => deps.getMacOSForegroundProbeActive?.() ?? false,
|
||||
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createSubtitleSidebarModal,
|
||||
findActiveSubtitleCueIndex,
|
||||
} from './subtitle-sidebar.js';
|
||||
import { YOMITAN_POPUP_HIDDEN_EVENT, YOMITAN_POPUP_SHOWN_EVENT } from '../yomitan-popup.js';
|
||||
|
||||
function createClassList(initialTokens: string[] = []) {
|
||||
const tokens = new Set(initialTokens);
|
||||
@@ -1542,6 +1543,137 @@ test('subtitle sidebar hover pause ignores playback-state IPC failures', async (
|
||||
}
|
||||
});
|
||||
|
||||
test('subtitle sidebar keeps hover pause while a Yomitan lookup popup remains open', async () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
const previousDocument = globals.document;
|
||||
const mpvCommands: Array<Array<string | number>> = [];
|
||||
const contentListeners = new Map<string, Array<() => Promise<void> | void>>();
|
||||
const windowListeners = new Map<string, Array<() => Promise<void> | void>>();
|
||||
|
||||
const snapshot: SubtitleSidebarSnapshot = {
|
||||
cues: [{ startTime: 1, endTime: 2, text: 'first' }],
|
||||
currentSubtitle: {
|
||||
text: 'first',
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
},
|
||||
currentTimeSec: 1.1,
|
||||
config: {
|
||||
enabled: true,
|
||||
autoOpen: false,
|
||||
layout: 'overlay',
|
||||
toggleKey: 'Backslash',
|
||||
pauseVideoOnHover: true,
|
||||
autoScroll: true,
|
||||
maxWidth: 420,
|
||||
opacity: 0.92,
|
||||
backgroundColor: 'rgba(54, 58, 79, 0.88)',
|
||||
textColor: '#cad3f5',
|
||||
fontFamily: '"Iosevka Aile", sans-serif',
|
||||
fontSize: 17,
|
||||
timestampColor: '#a5adcb',
|
||||
activeLineColor: '#f5bde6',
|
||||
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
|
||||
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
addEventListener: (type: string, listener: () => Promise<void> | void) => {
|
||||
const bucket = windowListeners.get(type) ?? [];
|
||||
bucket.push(listener);
|
||||
windowListeners.set(type, bucket);
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
electronAPI: {
|
||||
getSubtitleSidebarSnapshot: async () => snapshot,
|
||||
getPlaybackPaused: async () => false,
|
||||
sendMpvCommand: (command: Array<string | number>) => {
|
||||
mpvCommands.push(command);
|
||||
},
|
||||
} as unknown as ElectronAPI,
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
createElement: () => createCueRow(),
|
||||
body: {
|
||||
classList: createClassList(),
|
||||
},
|
||||
documentElement: {
|
||||
style: {
|
||||
setProperty: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const state = createRendererState();
|
||||
state.autoPauseVideoOnYomitanPopup = true;
|
||||
const ctx = {
|
||||
dom: {
|
||||
overlay: { classList: createClassList() },
|
||||
subtitleSidebarModal: {
|
||||
classList: createClassList(['hidden']),
|
||||
setAttribute: () => {},
|
||||
style: { setProperty: () => {} },
|
||||
addEventListener: () => {},
|
||||
},
|
||||
subtitleSidebarContent: {
|
||||
classList: createClassList(),
|
||||
getBoundingClientRect: () => ({ width: 420 }),
|
||||
addEventListener: (type: string, listener: () => Promise<void> | void) => {
|
||||
const bucket = contentListeners.get(type) ?? [];
|
||||
bucket.push(listener);
|
||||
contentListeners.set(type, bucket);
|
||||
},
|
||||
},
|
||||
subtitleSidebarClose: { addEventListener: () => {} },
|
||||
subtitleSidebarStatus: { textContent: '' },
|
||||
subtitleSidebarList: createListStub(),
|
||||
},
|
||||
state,
|
||||
};
|
||||
|
||||
const modal = createSubtitleSidebarModal(ctx as never, {
|
||||
modalStateReader: { isAnyModalOpen: () => false },
|
||||
});
|
||||
modal.wireDomEvents();
|
||||
|
||||
await modal.openSubtitleSidebarModal();
|
||||
mpvCommands.length = 0;
|
||||
await contentListeners.get('mouseenter')?.[0]?.();
|
||||
|
||||
assert.deepEqual(mpvCommands, [['set_property', 'pause', 'yes']]);
|
||||
|
||||
for (const listener of windowListeners.get(YOMITAN_POPUP_SHOWN_EVENT) ?? []) {
|
||||
await listener();
|
||||
}
|
||||
await contentListeners.get('mouseleave')?.[0]?.();
|
||||
|
||||
assert.deepEqual(mpvCommands, [['set_property', 'pause', 'yes']]);
|
||||
assert.equal(state.subtitleSidebarPausedByHover, true);
|
||||
|
||||
for (const listener of windowListeners.get(YOMITAN_POPUP_HIDDEN_EVENT) ?? []) {
|
||||
await listener();
|
||||
}
|
||||
|
||||
assert.deepEqual(mpvCommands, [
|
||||
['set_property', 'pause', 'yes'],
|
||||
['set_property', 'pause', 'no'],
|
||||
]);
|
||||
assert.equal(state.subtitleSidebarPausedByHover, false);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('subtitle sidebar embedded layout reserves and releases mpv right margin', async () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { SubtitleCue, SubtitleData, SubtitleSidebarSnapshot } from '../../types';
|
||||
import type { ModalStateReader, RendererContext } from '../context';
|
||||
import { syncOverlayMouseIgnoreState } from '../overlay-mouse-ignore.js';
|
||||
import {
|
||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||
YOMITAN_POPUP_SHOWN_EVENT,
|
||||
isYomitanPopupVisible,
|
||||
} from '../yomitan-popup.js';
|
||||
|
||||
const MANUAL_SCROLL_HOLD_MS = 1500;
|
||||
const ACTIVE_CUE_LOOKAHEAD_SEC = 0.18;
|
||||
@@ -194,6 +199,8 @@ export function createSubtitleSidebarModal(
|
||||
let disposeDomEvents: (() => void) | null = null;
|
||||
let subtitleSidebarHovered = false;
|
||||
let subtitleSidebarFocusedWithin = false;
|
||||
let subtitleSidebarYomitanPopupVisible = false;
|
||||
let subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
|
||||
function restoreEmbeddedSidebarPassthrough(): void {
|
||||
syncOverlayMouseIgnoreState(ctx);
|
||||
@@ -323,18 +330,65 @@ export function createSubtitleSidebarModal(
|
||||
return `Jump to subtitle at ${formatCueTimestamp(cue.startTime)}`;
|
||||
}
|
||||
|
||||
function isYomitanPopupVisibleForSidebar(): boolean {
|
||||
if (subtitleSidebarYomitanPopupVisible || ctx.state.yomitanPopupVisible) {
|
||||
return true;
|
||||
}
|
||||
if (typeof document === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return isYomitanPopupVisible(document);
|
||||
}
|
||||
|
||||
function shouldHoldSidebarPauseForYomitanPopup(): boolean {
|
||||
return (
|
||||
ctx.state.autoPauseVideoOnYomitanPopup &&
|
||||
ctx.state.subtitleSidebarPausedByHover &&
|
||||
isYomitanPopupVisibleForSidebar()
|
||||
);
|
||||
}
|
||||
|
||||
function resumeSubtitleSidebarHoverPause(): void {
|
||||
subtitleSidebarHoverRequestId += 1;
|
||||
if (!ctx.state.subtitleSidebarPausedByHover) {
|
||||
subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldHoldSidebarPauseForYomitanPopup()) {
|
||||
subtitleSidebarPauseHeldByYomitanPopup = true;
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
return;
|
||||
}
|
||||
|
||||
subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
ctx.state.subtitleSidebarPausedByHover = false;
|
||||
window.electronAPI.sendMpvCommand(['set_property', 'pause', 'no']);
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
}
|
||||
|
||||
function handleYomitanPopupShown(): void {
|
||||
subtitleSidebarYomitanPopupVisible = true;
|
||||
if (ctx.state.autoPauseVideoOnYomitanPopup && ctx.state.subtitleSidebarPausedByHover) {
|
||||
subtitleSidebarPauseHeldByYomitanPopup = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleYomitanPopupHidden(): void {
|
||||
subtitleSidebarYomitanPopupVisible = false;
|
||||
if (!subtitleSidebarPauseHeldByYomitanPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
subtitleSidebarPauseHeldByYomitanPopup = false;
|
||||
if (ctx.state.isOverSubtitleSidebar) {
|
||||
restoreEmbeddedSidebarPassthrough();
|
||||
return;
|
||||
}
|
||||
resumeSubtitleSidebarHoverPause();
|
||||
}
|
||||
|
||||
function maybeAutoScrollActiveCue(
|
||||
previousActiveCueIndex: number,
|
||||
behavior: ScrollBehavior = 'smooth',
|
||||
@@ -660,8 +714,12 @@ export function createSubtitleSidebarModal(
|
||||
syncEmbeddedSidebarLayout();
|
||||
};
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
window.addEventListener(YOMITAN_POPUP_SHOWN_EVENT, handleYomitanPopupShown);
|
||||
window.addEventListener(YOMITAN_POPUP_HIDDEN_EVENT, handleYomitanPopupHidden);
|
||||
disposeDomEvents = () => {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
window.removeEventListener(YOMITAN_POPUP_SHOWN_EVENT, handleYomitanPopupShown);
|
||||
window.removeEventListener(YOMITAN_POPUP_HIDDEN_EVENT, handleYomitanPopupHidden);
|
||||
disposeDomEvents = null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ export abstract class BaseWindowTracker {
|
||||
abstract start(): void;
|
||||
abstract stop(): void;
|
||||
|
||||
refreshNow(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getGeometry(): WindowGeometry | null {
|
||||
return this.currentGeometry;
|
||||
}
|
||||
|
||||
@@ -359,6 +359,35 @@ test('MacOSWindowTracker marks target unfocused on explicit inactive helper sign
|
||||
assert.deepEqual(focusChanges, [true, false]);
|
||||
});
|
||||
|
||||
test('MacOSWindowTracker refreshNow immediately samples frontmost mpv state', async () => {
|
||||
let callIndex = 0;
|
||||
const outputs = [
|
||||
{ stdout: '10,20,1280,720,0', stderr: '' },
|
||||
{ stdout: 'active', stderr: '' },
|
||||
];
|
||||
|
||||
const tracker = new MacOSWindowTracker('/tmp/mpv.sock', {
|
||||
resolveHelper: () => ({
|
||||
helperPath: 'helper.swift',
|
||||
helperType: 'swift',
|
||||
}),
|
||||
runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!,
|
||||
});
|
||||
|
||||
await (tracker as unknown as { refreshNow: () => Promise<void> }).refreshNow();
|
||||
assert.equal(tracker.isTargetWindowFocused(), false);
|
||||
|
||||
await (tracker as unknown as { refreshNow: () => Promise<void> }).refreshNow();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.equal(tracker.isTargetWindowFocused(), true);
|
||||
assert.deepEqual(tracker.getGeometry(), {
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
});
|
||||
});
|
||||
|
||||
test('MacOSWindowTracker drops tracking after consecutive helper misses', async () => {
|
||||
let callIndex = 0;
|
||||
const outputs = [
|
||||
|
||||
@@ -196,7 +196,7 @@ export function parseMacOSHelperOutput(result: string): MacOSHelperWindowState |
|
||||
|
||||
export class MacOSWindowTracker extends BaseWindowTracker {
|
||||
private pollTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private pollInFlight = false;
|
||||
private pollInFlightPromise: Promise<void> | null = null;
|
||||
private started = false;
|
||||
private helperPath: string | null = null;
|
||||
private helperType: 'binary' | 'swift' | null = null;
|
||||
@@ -357,7 +357,7 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
this.pollGeometry();
|
||||
void this.pollGeometry();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
@@ -365,6 +365,11 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
||||
this.clearScheduledPoll();
|
||||
}
|
||||
|
||||
override refreshNow(): Promise<void> {
|
||||
this.clearScheduledPoll();
|
||||
return this.pollGeometry();
|
||||
}
|
||||
|
||||
override isTargetWindowMinimized(): boolean {
|
||||
return this.targetWindowMinimized;
|
||||
}
|
||||
@@ -443,13 +448,19 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
||||
}, this.resolveNextPollIntervalMs());
|
||||
}
|
||||
|
||||
private pollGeometry(): void {
|
||||
if (this.pollInFlight || !this.helperPath || !this.helperType) {
|
||||
return;
|
||||
private pollGeometry(): Promise<void> {
|
||||
if (this.pollInFlightPromise) {
|
||||
return this.pollInFlightPromise;
|
||||
}
|
||||
if (!this.helperPath || !this.helperType) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.pollInFlight = true;
|
||||
void this.runHelper(this.helperPath, this.helperType, this.targetMpvSocketPath)
|
||||
this.pollInFlightPromise = this.runHelper(
|
||||
this.helperPath,
|
||||
this.helperType,
|
||||
this.targetMpvSocketPath,
|
||||
)
|
||||
.then(({ stdout }) => {
|
||||
const parsed = parseMacOSHelperOutput(stdout || '');
|
||||
if (parsed) {
|
||||
@@ -495,8 +506,9 @@ export class MacOSWindowTracker extends BaseWindowTracker {
|
||||
this.registerTrackingMiss();
|
||||
})
|
||||
.finally(() => {
|
||||
this.pollInFlight = false;
|
||||
this.pollInFlightPromise = null;
|
||||
this.scheduleNextPoll();
|
||||
});
|
||||
return this.pollInFlightPromise;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user