mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-30 18:12:08 -07:00
Fix Windows YouTube playback flow and overlay pointer tracking
This commit is contained in:
@@ -759,6 +759,76 @@ test('restorePointerInteractionState re-enables subtitle hover when pointer is a
|
||||
}
|
||||
handlers.restorePointerInteractionState();
|
||||
|
||||
assert.equal(ctx.state.isOverSubtitle, true);
|
||||
assert.equal(ctx.dom.overlay.classList.contains('interactive'), true);
|
||||
assert.deepEqual(ignoreCalls, [
|
||||
{ ignore: false, forward: undefined },
|
||||
{ ignore: false, forward: undefined },
|
||||
]);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: originalDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('pointer tracking enables overlay interaction as soon as the cursor reaches subtitles', () => {
|
||||
const ctx = createMouseTestContext();
|
||||
const originalWindow = globalThis.window;
|
||||
const originalDocument = globalThis.document;
|
||||
const ignoreCalls: Array<{ ignore: boolean; forward?: boolean }> = [];
|
||||
const documentListeners = new Map<string, Array<(event: unknown) => void>>();
|
||||
ctx.platform.shouldToggleMouseIgnore = true;
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
electronAPI: {
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||
ignoreCalls.push({ ignore, forward: options?.forward });
|
||||
},
|
||||
},
|
||||
getComputedStyle: () => ({
|
||||
visibility: 'hidden',
|
||||
display: 'none',
|
||||
opacity: '0',
|
||||
}),
|
||||
focus: () => {},
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
addEventListener: (type: string, listener: (event: unknown) => void) => {
|
||||
const bucket = documentListeners.get(type) ?? [];
|
||||
bucket.push(listener);
|
||||
documentListeners.set(type, bucket);
|
||||
},
|
||||
elementFromPoint: () => ctx.dom.subtitleContainer,
|
||||
querySelectorAll: () => [],
|
||||
body: {},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const handlers = createMouseHandlers(ctx as never, {
|
||||
modalStateReader: {
|
||||
isAnySettingsModalOpen: () => false,
|
||||
isAnyModalOpen: () => false,
|
||||
},
|
||||
applyYPercent: () => {},
|
||||
getCurrentYPercent: () => 10,
|
||||
persistSubtitlePositionPatch: () => {},
|
||||
getSubtitleHoverAutoPauseEnabled: () => false,
|
||||
getYomitanPopupAutoPauseEnabled: () => false,
|
||||
getPlaybackPaused: async () => false,
|
||||
sendMpvCommand: () => {},
|
||||
});
|
||||
|
||||
handlers.setupPointerTracking();
|
||||
for (const listener of documentListeners.get('mousemove') ?? []) {
|
||||
listener({ clientX: 120, clientY: 240 });
|
||||
}
|
||||
|
||||
assert.equal(ctx.state.isOverSubtitle, true);
|
||||
assert.equal(ctx.dom.overlay.classList.contains('interactive'), true);
|
||||
assert.deepEqual(ignoreCalls, [{ ignore: false, forward: undefined }]);
|
||||
@@ -768,6 +838,82 @@ test('restorePointerInteractionState re-enables subtitle hover when pointer is a
|
||||
}
|
||||
});
|
||||
|
||||
test('pointer tracking restores click-through after the cursor leaves subtitles', () => {
|
||||
const ctx = createMouseTestContext();
|
||||
const originalWindow = globalThis.window;
|
||||
const originalDocument = globalThis.document;
|
||||
const ignoreCalls: Array<{ ignore: boolean; forward?: boolean }> = [];
|
||||
const documentListeners = new Map<string, Array<(event: unknown) => void>>();
|
||||
let hoveredElement: unknown = ctx.dom.subtitleContainer;
|
||||
ctx.platform.shouldToggleMouseIgnore = true;
|
||||
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
electronAPI: {
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||
ignoreCalls.push({ ignore, forward: options?.forward });
|
||||
},
|
||||
},
|
||||
getComputedStyle: () => ({
|
||||
visibility: 'hidden',
|
||||
display: 'none',
|
||||
opacity: '0',
|
||||
}),
|
||||
focus: () => {},
|
||||
},
|
||||
});
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
configurable: true,
|
||||
value: {
|
||||
addEventListener: (type: string, listener: (event: unknown) => void) => {
|
||||
const bucket = documentListeners.get(type) ?? [];
|
||||
bucket.push(listener);
|
||||
documentListeners.set(type, bucket);
|
||||
},
|
||||
elementFromPoint: () => hoveredElement,
|
||||
querySelectorAll: () => [],
|
||||
body: {},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const handlers = createMouseHandlers(ctx as never, {
|
||||
modalStateReader: {
|
||||
isAnySettingsModalOpen: () => false,
|
||||
isAnyModalOpen: () => false,
|
||||
},
|
||||
applyYPercent: () => {},
|
||||
getCurrentYPercent: () => 10,
|
||||
persistSubtitlePositionPatch: () => {},
|
||||
getSubtitleHoverAutoPauseEnabled: () => false,
|
||||
getYomitanPopupAutoPauseEnabled: () => false,
|
||||
getPlaybackPaused: async () => false,
|
||||
sendMpvCommand: () => {},
|
||||
});
|
||||
|
||||
handlers.setupPointerTracking();
|
||||
for (const listener of documentListeners.get('mousemove') ?? []) {
|
||||
listener({ clientX: 120, clientY: 240 });
|
||||
}
|
||||
|
||||
hoveredElement = null;
|
||||
for (const listener of documentListeners.get('mousemove') ?? []) {
|
||||
listener({ clientX: 640, clientY: 360 });
|
||||
}
|
||||
|
||||
assert.equal(ctx.state.isOverSubtitle, false);
|
||||
assert.equal(ctx.dom.overlay.classList.contains('interactive'), false);
|
||||
assert.deepEqual(ignoreCalls, [
|
||||
{ ignore: false, forward: undefined },
|
||||
{ ignore: true, forward: true },
|
||||
]);
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: originalWindow });
|
||||
Object.defineProperty(globalThis, 'document', { configurable: true, value: originalDocument });
|
||||
}
|
||||
});
|
||||
|
||||
test('restorePointerInteractionState keeps overlay interactive until first real pointer move can resync hover', () => {
|
||||
const ctx = createMouseTestContext();
|
||||
const originalWindow = globalThis.window;
|
||||
|
||||
@@ -20,6 +20,12 @@ export function createMouseHandlers(
|
||||
sendMpvCommand: (command: (string | number)[]) => void;
|
||||
},
|
||||
) {
|
||||
type HoverPointState = {
|
||||
overPrimarySubtitle: boolean;
|
||||
overSecondarySubtitle: boolean;
|
||||
isOverSubtitle: boolean;
|
||||
};
|
||||
|
||||
let yomitanPopupVisible = false;
|
||||
let hoverPauseRequestId = 0;
|
||||
let popupPauseRequestId = 0;
|
||||
@@ -45,7 +51,7 @@ export function createMouseHandlers(
|
||||
};
|
||||
}
|
||||
|
||||
function syncHoverStateFromPoint(clientX: number, clientY: number): boolean {
|
||||
function getHoverStateFromPoint(clientX: number, clientY: number): HoverPointState {
|
||||
const hoveredElement =
|
||||
typeof document.elementFromPoint === 'function'
|
||||
? document.elementFromPoint(clientX, clientY)
|
||||
@@ -56,13 +62,52 @@ export function createMouseHandlers(
|
||||
ctx.dom.secondarySubContainer,
|
||||
);
|
||||
|
||||
ctx.state.isOverSubtitle = overPrimarySubtitle || overSecondarySubtitle;
|
||||
return {
|
||||
overPrimarySubtitle,
|
||||
overSecondarySubtitle,
|
||||
isOverSubtitle: overPrimarySubtitle || overSecondarySubtitle,
|
||||
};
|
||||
}
|
||||
|
||||
function syncHoverStateFromPoint(clientX: number, clientY: number): HoverPointState {
|
||||
const hoverState = getHoverStateFromPoint(clientX, clientY);
|
||||
|
||||
ctx.state.isOverSubtitle = hoverState.isOverSubtitle;
|
||||
ctx.dom.secondarySubContainer.classList.toggle(
|
||||
'secondary-sub-hover-active',
|
||||
overSecondarySubtitle,
|
||||
hoverState.overSecondarySubtitle,
|
||||
);
|
||||
|
||||
return ctx.state.isOverSubtitle;
|
||||
return hoverState;
|
||||
}
|
||||
|
||||
function syncHoverStateFromTrackedPointer(event: MouseEvent | PointerEvent): void {
|
||||
if (!ctx.platform.shouldToggleMouseIgnore || ctx.state.isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wasOverSubtitle = ctx.state.isOverSubtitle;
|
||||
const wasOverSecondarySubtitle = ctx.dom.secondarySubContainer.classList.contains(
|
||||
'secondary-sub-hover-active',
|
||||
);
|
||||
const hoverState = syncHoverStateFromPoint(event.clientX, event.clientY);
|
||||
|
||||
if (!wasOverSubtitle && hoverState.isOverSubtitle) {
|
||||
void handleMouseEnter(undefined, hoverState.overSecondarySubtitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wasOverSubtitle && !hoverState.isOverSubtitle) {
|
||||
void handleMouseLeave(undefined, wasOverSecondarySubtitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
hoverState.isOverSubtitle &&
|
||||
hoverState.overSecondarySubtitle !== wasOverSecondarySubtitle
|
||||
) {
|
||||
syncOverlayMouseIgnoreState(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
function restorePointerInteractionState(): void {
|
||||
@@ -293,10 +338,12 @@ export function createMouseHandlers(
|
||||
function setupPointerTracking(): void {
|
||||
document.addEventListener('mousemove', (event: MouseEvent) => {
|
||||
updatePointerPosition(event);
|
||||
syncHoverStateFromTrackedPointer(event);
|
||||
maybeResyncPointerHoverState(event);
|
||||
});
|
||||
document.addEventListener('pointermove', (event: PointerEvent) => {
|
||||
updatePointerPosition(event);
|
||||
syncHoverStateFromTrackedPointer(event);
|
||||
maybeResyncPointerHoverState(event);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user