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