From 9af0b9081675841005039f78989e530e80ac0b29 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 10 Jul 2026 15:52:34 +0200 Subject: [PATCH] feat(player): open YouTube's native settings menu fully + push higher quality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The transparent interaction overlay (click=play/pause, wheel=volume, keeps keyboard focus off the cross-origin iframe) also blocked YouTube's own settings menu, which expands down over the video past the overlay: items below the first few were unclickable, and 'More options' made it worse. Now the overlay yields to native controls. Clicking a native control (gear/seek/ CC) moves focus into the player iframe — the only cross-origin signal available — which we detect (window blur + getIframe() focus check) to drop the overlay's pointer-events, so the whole menu is navigable at any height. A discreet badge signals native mode; moving the pointer off the video (or the window regaining focus) re-arms the click/scroll/keyboard shortcuts. Quality: the IFrame API's setPlaybackQuality/suggestedQuality are hard no-ops now, so the real lever is the rendered player size — bump the modal max-w-4xl -> 6xl so YouTube's ABR targets a higher resolution — plus a best-effort 'vq=hd1080' URL hint. i18n (en/hu/de) for the native-mode badge. --- frontend/src/components/PlayerModal.tsx | 55 ++++++++++++++++++++++-- frontend/src/i18n/locales/de/player.json | 3 +- frontend/src/i18n/locales/en/player.json | 3 +- frontend/src/i18n/locales/hu/player.json | 3 +- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index 560de63..44ca820 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { createPortal } from "react-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { AlertTriangle, ArrowLeft, Check, CheckCheck, ChevronLeft, ChevronRight, ExternalLink, Repeat, Repeat1, Shuffle, SkipBack, SkipForward, Volume2, VolumeX, X } from "lucide-react"; +import { AlertTriangle, ArrowLeft, Check, CheckCheck, ChevronLeft, ChevronRight, ExternalLink, Repeat, Repeat1, Settings, Shuffle, SkipBack, SkipForward, Volume2, VolumeX, X } from "lucide-react"; import Avatar from "./Avatar"; import AddToPlaylist from "./AddToPlaylist"; import DownloadButton from "./DownloadButton"; @@ -124,6 +124,12 @@ export default function PlayerModal({ const volTimerRef = useRef(undefined); // Volume level to flash in the on-player overlay (null = hidden). Auto-fades after a moment. const [volumeUi, setVolumeUi] = useState(null); + // When the user interacts with YouTube's own controls (gear/seek/CC), focus moves into the + // cross-origin player iframe. We detect that and drop the interaction overlay's pointer-events + // so YouTube's settings menu — which expands DOWN over the video, past our overlay — is fully + // navigable (previously the overlay ate clicks below the top items). Moving the pointer off the + // video, or the window regaining focus, re-arms our click/scroll/keyboard shortcuts. + const [nativeControls, setNativeControls] = useState(false); const focusModal = () => cardRef.current?.focus(); const togglePlay = () => { @@ -396,6 +402,35 @@ export default function PlayerModal({ // Re-attach if the overlay remounts (it's hidden while the embed shows an error). }, [playerError]); + // Yield the interaction overlay to YouTube's native UI. Clicking a native control (gear / seek + // bar / CC) moves focus into the player iframe — the only cross-origin signal we get. While the + // iframe holds focus we drop the overlay's pointer-events so the (arbitrarily tall) settings + // menu is clickable end to end. Re-arm — restoring click-to-play, wheel volume and our keyboard + // shortcuts — when the pointer leaves the video or the window regains focus (a click back on our + // card blurs the iframe → the parent window fires 'focus'). + useEffect(() => { + const iframeFocused = () => { + const p = playerRef.current; + const f = p && typeof p.getIframe === "function" ? p.getIframe() : null; + return !!f && document.activeElement === f; + }; + // activeElement settles on the tick after blur, so defer the check. + const onBlur = () => window.setTimeout(() => iframeFocused() && setNativeControls(true), 0); + const rearm = () => { + setNativeControls(false); + focusModal(); + }; + const stage = stageRef.current; + window.addEventListener("blur", onBlur); + window.addEventListener("focus", rearm); + stage?.addEventListener("mouseleave", rearm); + return () => { + window.removeEventListener("blur", onBlur); + window.removeEventListener("focus", rearm); + stage?.removeEventListener("mouseleave", rearm); + }; + }, []); + // Create the player, resume from the saved position, persist progress, and // auto-mark watched once playback reaches the end. useEffect(() => { @@ -448,6 +483,11 @@ export default function PlayerModal({ enablejsapi: 1, origin: window.location.origin, playsinline: 1, + // Best-effort HD hint. The IFrame API's setPlaybackQuality/suggestedQuality are hard no-ops + // now (YouTube removed them), but the undocumented `vq` URL param is still honoured for many + // videos and is harmless otherwise. The real quality lever is the player's rendered size + // (see max-w-6xl below) — YouTube's ABR targets a resolution to match the pixel box. + vq: "hd1080", }, events: { // Keep keyboard focus on the modal, not the iframe, so shortcuts work right away. @@ -510,7 +550,7 @@ export default function PlayerModal({ playlist). Each strip sits just outside the card, spans its full height, and fades out at the ends. stopPropagation so a click steps instead of closing. Hidden on narrow screens where there's no room beside the card. */} -
+
{hasQueue && (