diff --git a/extension/content/inject.js b/extension/content/inject.js index 662d652..ab6b0cf 100644 --- a/extension/content/inject.js +++ b/extension/content/inject.js @@ -3567,7 +3567,10 @@ function defineVideoController() { // YouTube adds ytp-autohide class to the player when controls should be hidden // We mirror this class state to enable CSS-based hiding // The vsc-hidden class (from V key) takes precedence via CSS specificity - if (ytPlayer.classList.contains("ytp-autohide")) { + if ( + ytPlayer.classList.contains("ytp-autohide") && + !this.controllerInteractionActive + ) { wrapper.classList.add("ytp-autohide"); // Immediately end any temporary "vsc-show" state to hide with YouTube @@ -3612,6 +3615,16 @@ function defineVideoController() { showController(wrapper, tc.settings.hideWithControlsTimer * 1000); }; + const handleControllerInteractionChange = (active) => { + if (active) { + wrapper.classList.remove("ytp-autohide"); + showController(wrapper, tc.settings.hideWithControlsTimer * 1000); + } else { + syncControllerVisibility(); + } + }; + this.controllerInteractionChanged = handleControllerInteractionChange; + const activityEvents = ["mousemove", "mousedown", "touchstart"]; activityEvents.forEach((type) => { video.addEventListener(type, resetTimer, { passive: true }); @@ -3626,6 +3639,9 @@ function defineVideoController() { wrapper.removeEventListener(type, resetTimer); ytPlayer.removeEventListener(type, resetTimer); }); + if (this.controllerInteractionChanged === handleControllerInteractionChange) { + this.controllerInteractionChanged = null; + } }; }; @@ -3642,6 +3658,8 @@ function defineVideoController() { clearTimeout(timer); } 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" @@ -3652,6 +3670,20 @@ function defineVideoController() { }, tc.settings.hideWithControlsTimer * 1000); }; + const handleControllerInteractionChange = (active) => { + if (active) { + wrapper.classList.remove("vsc-idle-hidden"); + showController(wrapper, tc.settings.hideWithControlsTimer * 1000); + if (timer) { + clearTimeout(timer); + timer = null; + } + } else { + resetTimer(); + } + }; + this.controllerInteractionChanged = handleControllerInteractionChange; + // Initial show/timer resetTimer(); @@ -3685,6 +3717,9 @@ function defineVideoController() { }); video.removeEventListener("play", resetTimer); video.removeEventListener("pause", resetTimer); + if (this.controllerInteractionChanged === handleControllerInteractionChange) { + this.controllerInteractionChanged = null; + } }; log(`Generic auto-hide setup complete with ${tc.settings.hideWithControlsTimer}s timer`, 4); @@ -3758,6 +3793,27 @@ function defineVideoController() { controller.appendChild(nudgeFlashIndicator); shadow.appendChild(controller); + const setControllerInteractionActive = (active) => { + this.controllerInteractionActive = active === true; + controller.classList.toggle( + "vsc-controls-hovered", + this.controllerInteractionActive + ); + wrapper.classList.toggle( + "vsc-controls-hovered", + this.controllerInteractionActive + ); + if (typeof this.controllerInteractionChanged === "function") { + this.controllerInteractionChanged(this.controllerInteractionActive); + } + }; + controller.addEventListener("pointerenter", () => { + setControllerInteractionActive(true); + }); + controller.addEventListener("pointerleave", () => { + setControllerInteractionActive(false); + }); + this.speedIndicator = dragHandle; this.subtitleNudgeIndicator = subtitleNudgeIndicator; this.nudgeFlashIndicator = nudgeFlashIndicator; diff --git a/extension/content/shadow.css b/extension/content/shadow.css index b618054..9d06da0 100644 --- a/extension/content/shadow.css +++ b/extension/content/shadow.css @@ -44,6 +44,7 @@ after a mouse click kept #controls visible while hover-only rules (e.g. draggable margin) turned off when the pointer left the bar. */ #controller:hover #controls, +#controller.vsc-controls-hovered #controls, #controller:focus-within:has(:focus-visible) #controls, :host(:hover) #controls { display: inline-flex; @@ -85,6 +86,10 @@ margin-right: 5px; } +#controller.vsc-controls-hovered > .draggable { + margin-right: 5px; +} + /* Center presets: midpoint between left- and right-preset inset lines; center bar on that X. */ #controller[data-location="top-center"]:not([data-position-mode="manual"]) { transform: translate(-50%, 0) !important; diff --git a/tests/inject-lifecycle.regression.test.js b/tests/inject-lifecycle.regression.test.js index cbd2c44..ab49cd1 100644 --- a/tests/inject-lifecycle.regression.test.js +++ b/tests/inject-lifecycle.regression.test.js @@ -562,6 +562,56 @@ describe("inject.js media/controller lifecycle regressions", () => { expect(mount.lastElementChild).toBe(wrapper); }); + it("keeps expanded controls visible and disables auto-hide while hovered", async () => { + vi.useFakeTimers(); + bootInject({ + syncData: { + hideWithControls: true, + hideWithControlsTimer: 1 + } + }); + await settleLifecycle(); + + const { controller, wrapper } = createControlledVideo(); + const controllerElement = window.getControllerElement(controller); + controllerElement.dispatchEvent(new Event("pointerenter")); + + expect(controller.controllerInteractionActive).toBe(true); + expect(controllerElement.classList.contains("vsc-controls-hovered")).toBe(true); + expect(wrapper.classList.contains("vsc-controls-hovered")).toBe(true); + await vi.advanceTimersByTimeAsync(1500); + expect(wrapper.classList.contains("vsc-idle-hidden")).toBe(false); + + controllerElement.dispatchEvent(new Event("pointerleave")); + expect(controller.controllerInteractionActive).toBe(false); + await vi.advanceTimersByTimeAsync(1000); + expect(wrapper.classList.contains("vsc-idle-hidden")).toBe(true); + }); + + it("does not let YouTube auto-hide collapse controls under the pointer", async () => { + vi.useFakeTimers(); + bootInject({ + url: "https://www.youtube.com/watch?v=hover-lock", + syncData: { + hideWithControls: true, + hideWithControlsTimer: 1 + } + }); + await settleLifecycle(); + + const player = document.createElement("div"); + player.className = "html5-video-player ytp-autohide"; + const { controller, wrapper } = createControlledVideo({ mount: player }); + const controllerElement = window.getControllerElement(controller); + + expect(wrapper.classList.contains("ytp-autohide")).toBe(true); + controllerElement.dispatchEvent(new Event("pointerenter")); + expect(wrapper.classList.contains("ytp-autohide")).toBe(false); + + controllerElement.dispatchEvent(new Event("pointerleave")); + expect(wrapper.classList.contains("ytp-autohide")).toBe(true); + }); + it("suppresses a zero-size host and reveals it when the video becomes visible", async () => { bootInject(); await settleLifecycle();