mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-08-20 12:06:19 -04:00
fix(shortcuts): avoid layout work during key handling
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
/* Base styles for the controller wrapper (the shadow host) */
|
||||
.vsc-controller {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
pointer-events: none !important;
|
||||
/* Keep the interactive controller above player-owned click/pause panes. */
|
||||
z-index: 2147483647 !important;
|
||||
@@ -17,7 +13,6 @@
|
||||
controller in the browser top layer without nesting it inside <video>. */
|
||||
.vsc-controller.vsc-fullscreen-popover {
|
||||
position: fixed !important;
|
||||
inset: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
|
||||
+41
-52
@@ -1,4 +1,3 @@
|
||||
var isUserSeek = false; // Track if seek was user-initiated
|
||||
var lastToggleSpeed = {}; // Store last toggle speeds per video
|
||||
var speederShared =
|
||||
typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {};
|
||||
@@ -28,6 +27,11 @@ function getSharedDefault(key, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getCachedVideoRect(video) {
|
||||
var wrapper = video && video.vsc && video.vsc.div;
|
||||
return (wrapper && wrapper.vscVideoRect) || null;
|
||||
}
|
||||
|
||||
function getPrimaryVideoElement(mediaElements) {
|
||||
var candidates = Array.isArray(mediaElements)
|
||||
? mediaElements
|
||||
@@ -39,10 +43,13 @@ function getPrimaryVideoElement(mediaElements) {
|
||||
candidates.forEach(function(el, index) {
|
||||
if (!el || !el.vsc || !el.isConnected) return;
|
||||
|
||||
var rect = null;
|
||||
try {
|
||||
rect = el.getBoundingClientRect();
|
||||
} catch (_error) {}
|
||||
var rect = getCachedVideoRect(el);
|
||||
var hasCachedRect = Boolean(rect);
|
||||
if (!rect) {
|
||||
try {
|
||||
rect = el.getBoundingClientRect();
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
var width = rect && Number(rect.width) > 0 ? Number(rect.width) : 0;
|
||||
var height = rect && Number(rect.height) > 0 ? Number(rect.height) : 0;
|
||||
@@ -57,17 +64,24 @@ function getPrimaryVideoElement(mediaElements) {
|
||||
: 0;
|
||||
var visibleArea = visibleWidth * visibleHeight;
|
||||
var visuallyAvailable = visibleArea > 0;
|
||||
try {
|
||||
var computed = win && win.getComputedStyle(el);
|
||||
if (
|
||||
computed &&
|
||||
(computed.display === "none" ||
|
||||
computed.visibility === "hidden" ||
|
||||
Number(computed.opacity) === 0)
|
||||
) {
|
||||
visuallyAvailable = false;
|
||||
}
|
||||
} catch (_error) {}
|
||||
if (
|
||||
el.vsc.div &&
|
||||
el.vsc.div.classList.contains("vsc-geometry-hidden")
|
||||
) {
|
||||
visuallyAvailable = false;
|
||||
} else if (!hasCachedRect) {
|
||||
try {
|
||||
var computed = win && win.getComputedStyle(el);
|
||||
if (
|
||||
computed &&
|
||||
(computed.display === "none" ||
|
||||
computed.visibility === "hidden" ||
|
||||
Number(computed.opacity) === 0)
|
||||
) {
|
||||
visuallyAvailable = false;
|
||||
}
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
var score = visibleArea;
|
||||
if (visuallyAvailable) score += 1e12;
|
||||
@@ -2894,8 +2908,6 @@ function getControllerMount(video, boundary) {
|
||||
return isShadowRootNode(directRoot) ? directRoot : null;
|
||||
}
|
||||
|
||||
var mountBoundary = null;
|
||||
|
||||
var videoRect = video.getBoundingClientRect();
|
||||
var mount = video.parentElement;
|
||||
var candidate = mount;
|
||||
@@ -2913,15 +2925,7 @@ function getControllerMount(video, boundary) {
|
||||
// Climb through tightly-sized wrappers so our host shares their stacking
|
||||
// context, but stop before broad page-layout containers.
|
||||
while (candidate && candidate.parentElement && depth < 5) {
|
||||
if (mountBoundary && candidate === mountBoundary) break;
|
||||
var next = candidate.parentElement;
|
||||
if (
|
||||
mountBoundary &&
|
||||
next !== mountBoundary &&
|
||||
!mountBoundary.contains(next)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
var nextRect = next.getBoundingClientRect();
|
||||
var widthLimit = Math.max(videoRect.width * 1.35, videoRect.width + 80);
|
||||
var heightLimit = Math.max(videoRect.height * 1.35, videoRect.height + 80);
|
||||
@@ -2945,10 +2949,6 @@ function getControllerMount(video, boundary) {
|
||||
candidate = next;
|
||||
depth += 1;
|
||||
|
||||
// In fullscreen, the wrapper must remain inside the exact subtree the
|
||||
// browser promotes to its top layer.
|
||||
if (mountBoundary && next === mountBoundary) break;
|
||||
|
||||
// Never climb out of a player-owned stacking context. Doing so lets the
|
||||
// controller's high local z-index escape above sticky page headers.
|
||||
if (createsControllerStackingContext(next)) break;
|
||||
@@ -2976,6 +2976,14 @@ function positionControllerHost(wrapper, video, mount) {
|
||||
return;
|
||||
}
|
||||
var videoRect = video.getBoundingClientRect();
|
||||
wrapper.vscVideoRect = {
|
||||
left: Number(videoRect.left) || 0,
|
||||
top: Number(videoRect.top) || 0,
|
||||
right: Number(videoRect.right) || 0,
|
||||
bottom: Number(videoRect.bottom) || 0,
|
||||
width: Number(videoRect.width) || 0,
|
||||
height: Number(videoRect.height) || 0
|
||||
};
|
||||
if (wrapper.classList.contains("vsc-fullscreen-popover")) {
|
||||
if (videoRect.width <= 0 || videoRect.height <= 0) {
|
||||
wrapper.classList.add("vsc-geometry-hidden");
|
||||
@@ -3500,9 +3508,6 @@ function defineVideoController() {
|
||||
setSpeed(event.target, expectedSpeed, false, false);
|
||||
}
|
||||
|
||||
if (isUserSeek) {
|
||||
isUserSeek = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3896,11 +3901,6 @@ function defineVideoController() {
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
if (this.controllerInteractionActive) return;
|
||||
// Only hide if the video is not paused
|
||||
// (Many players keep controls visible while paused)
|
||||
// However, the user said "Reveal on every mouse and keyboard input"
|
||||
// and "auto-hidden after timespan".
|
||||
// We'll follow the timer strictly.
|
||||
wrapper.classList.add("vsc-idle-hidden");
|
||||
log("Generic hide: controller hidden due to inactivity", 5);
|
||||
}, tc.settings.hideWithControlsTimer * 1000);
|
||||
@@ -3923,8 +3923,8 @@ function defineVideoController() {
|
||||
// Initial show/timer
|
||||
resetTimer();
|
||||
|
||||
// The wrapper covers the player area on most sites due to inject.css styles,
|
||||
// but we listen on both the video and the wrapper for maximum coverage.
|
||||
// Players dispatch activity at different layers, so observe the media,
|
||||
// aligned controller host, and player mount.
|
||||
const activityEvents = ["mousemove", "mousedown", "keydown", "touchstart"];
|
||||
const parentEl =
|
||||
getControllerGeometryMount(this.controllerHostMount) ||
|
||||
@@ -4923,16 +4923,7 @@ function getClosestMediaToPointer(candidates, pointerPosition) {
|
||||
|
||||
candidates.forEach(function(video) {
|
||||
if (!video || !video.vsc || !video.isConnected) return;
|
||||
var target = getControllerElement(video.vsc) || video;
|
||||
var rect = null;
|
||||
try {
|
||||
rect = target.getBoundingClientRect();
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
||||
rect = video.getBoundingClientRect();
|
||||
}
|
||||
} catch (_error) {
|
||||
return;
|
||||
}
|
||||
var rect = getCachedVideoRect(video);
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0) return;
|
||||
var distance = distanceSquaredToRect(
|
||||
pointerPosition.x,
|
||||
@@ -5061,12 +5052,10 @@ function runAction(action, value, e) {
|
||||
);
|
||||
switch (action) {
|
||||
case "rewind":
|
||||
isUserSeek = true;
|
||||
extendSpeedRestoreWindow(v);
|
||||
v.currentTime -= numValue;
|
||||
break;
|
||||
case "advance":
|
||||
isUserSeek = true;
|
||||
extendSpeedRestoreWindow(v);
|
||||
v.currentTime += numValue;
|
||||
break;
|
||||
|
||||
@@ -281,15 +281,25 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
expect(video.vsc.div.style.getPropertyValue("height")).toBe("226px");
|
||||
});
|
||||
|
||||
it("does not let shadow host defaults override measured geometry", () => {
|
||||
const shadowCss = readWorkspaceFile("extension/content/shadow.css");
|
||||
const hostRule = shadowCss.match(/:host\s*\{([^}]*)\}/)[1];
|
||||
const fullscreenRule = shadowCss.match(
|
||||
/:host\(\.vsc-fullscreen-popover\)\s*\{([^}]*)\}/
|
||||
)[1];
|
||||
|
||||
expect(hostRule).not.toMatch(/\b(?:top|left|width|height)\s*:/);
|
||||
expect(fullscreenRule).not.toMatch(/\binset\s*:/);
|
||||
it("keeps measured geometry out of controller stylesheet defaults", () => {
|
||||
[
|
||||
{
|
||||
css: readWorkspaceFile("extension/content/inject.css"),
|
||||
host: /\.vsc-controller\s*\{([^}]*)\}/,
|
||||
fullscreen:
|
||||
/\.vsc-controller\.vsc-fullscreen-popover\s*\{([^}]*)\}/
|
||||
},
|
||||
{
|
||||
css: readWorkspaceFile("extension/content/shadow.css"),
|
||||
host: /:host\s*\{([^}]*)\}/,
|
||||
fullscreen: /:host\(\.vsc-fullscreen-popover\)\s*\{([^}]*)\}/
|
||||
}
|
||||
].forEach(({ css, host, fullscreen }) => {
|
||||
expect(css.match(host)[1]).not.toMatch(
|
||||
/\b(?:top|left|width|height)\s*:/
|
||||
);
|
||||
expect(css.match(fullscreen)[1]).not.toMatch(/\binset\s*:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("repositions a Shorts controller when playback moves the video on-screen", async () => {
|
||||
@@ -332,16 +342,27 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
src: "https://example.org/second.mp4",
|
||||
mountRect: makeRect(500, 0, 320, 180)
|
||||
});
|
||||
setRect(window.getControllerElement(first.controller), makeRect(10, 10, 120, 30));
|
||||
setRect(window.getControllerElement(second.controller), makeRect(510, 10, 120, 30));
|
||||
const firstLayoutRead = vi.fn(() => first.mount.getBoundingClientRect());
|
||||
const secondLayoutRead = vi.fn(() => second.mount.getBoundingClientRect());
|
||||
first.video.getBoundingClientRect = firstLayoutRead;
|
||||
second.video.getBoundingClientRect = secondLayoutRead;
|
||||
|
||||
document.dispatchEvent(
|
||||
new MouseEvent("mousemove", { bubbles: true, clientX: 600, clientY: 20 })
|
||||
);
|
||||
window.runAction("faster", 0.1);
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
code: "KeyD",
|
||||
key: "d"
|
||||
})
|
||||
);
|
||||
|
||||
expect(first.video.playbackRate).toBe(1);
|
||||
expect(second.video.playbackRate).toBe(1.1);
|
||||
expect(firstLayoutRead).not.toHaveBeenCalled();
|
||||
expect(secondLayoutRead).not.toHaveBeenCalled();
|
||||
|
||||
window.tc.settings.shortcutTargetMode = "all";
|
||||
window.runAction("faster", 0.1);
|
||||
@@ -1026,9 +1047,13 @@ describe("inject.js media/controller lifecycle regressions", () => {
|
||||
videoRect: makeRect(40, 40, 640, 360),
|
||||
src: "https://example.org/visible.mp4"
|
||||
}).video;
|
||||
const offscreenLayoutRead = vi.spyOn(offscreen, "getBoundingClientRect");
|
||||
const visibleLayoutRead = vi.spyOn(visible, "getBoundingClientRect");
|
||||
|
||||
expect(window.getPrimaryVideoElement()).toBe(visible);
|
||||
expect(window.getPrimaryVideoElement()).not.toBe(offscreen);
|
||||
expect(offscreenLayoutRead).not.toHaveBeenCalled();
|
||||
expect(visibleLayoutRead).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mounts a controller locally for video directly under an open ShadowRoot", async () => {
|
||||
|
||||
Reference in New Issue
Block a user