feat(player): open YouTube's native settings menu fully + push higher quality
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.
This commit is contained in:
parent
a2de72420b
commit
9af0b90816
4 changed files with 58 additions and 6 deletions
|
|
@ -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<number | undefined>(undefined);
|
||||
// Volume level to flash in the on-player overlay (null = hidden). Auto-fades after a moment.
|
||||
const [volumeUi, setVolumeUi] = useState<number | null>(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. */}
|
||||
<div className="relative flex w-full max-w-4xl max-h-full">
|
||||
<div className="relative flex w-full max-w-6xl max-h-full">
|
||||
{hasQueue && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
|
@ -552,9 +592,18 @@ export default function PlayerModal({
|
|||
}}
|
||||
title={t("player.shortcutsHint")}
|
||||
aria-label={t("player.shortcutsHint")}
|
||||
className="absolute inset-x-0 top-[12%] bottom-[22%] z-10 cursor-pointer"
|
||||
className={`absolute inset-x-0 top-[12%] bottom-[22%] z-10 cursor-pointer ${
|
||||
nativeControls ? "pointer-events-none" : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{/* Discreet badge while YouTube's own controls have taken over (overlay yielded). */}
|
||||
{playerError == null && nativeControls && (
|
||||
<div className="pointer-events-none absolute top-3 left-3 z-20 flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-white/90 shadow-lg backdrop-blur-sm">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
{t("player.nativeControls")}
|
||||
</div>
|
||||
)}
|
||||
{/* Volume level flash (auto-fades). pointer-events-none so it never blocks the video. */}
|
||||
{volumeUi != null && (
|
||||
<div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 z-20 flex items-center gap-2 rounded-full bg-black/70 px-3 py-1.5 text-xs text-white shadow-lg">
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@
|
|||
"unavailableBody": "Dieses Video ist nicht verfügbar — möglicherweise privat, entfernt oder regional gesperrt.",
|
||||
"embedDisabledBody": "Der Eigentümer hat die Wiedergabe auf anderen Seiten deaktiviert (häufig bei automatisch generierten „Topic“-Musiktiteln). Auf YouTube ist es weiterhin abspielbar.",
|
||||
"openOnYoutube": "Auf YouTube öffnen",
|
||||
"shortcutsHint": "Klick: Wiedergabe/Pause · Scrollen: Lautstärke · F: Vollbild"
|
||||
"shortcutsHint": "Klick: Wiedergabe/Pause · Scrollen: Lautstärke · F: Vollbild",
|
||||
"nativeControls": "YouTube-Steuerung aktiv"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@
|
|||
"unavailableBody": "This video isn't available — it may be private, removed, or region-restricted.",
|
||||
"embedDisabledBody": "The owner disabled playback on other sites (common for auto-generated “Topic” music tracks). It still plays on YouTube.",
|
||||
"openOnYoutube": "Open on YouTube",
|
||||
"shortcutsHint": "Click: play/pause · Scroll: volume · F: fullscreen"
|
||||
"shortcutsHint": "Click: play/pause · Scroll: volume · F: fullscreen",
|
||||
"nativeControls": "YouTube controls active"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@
|
|||
"unavailableBody": "Ez a videó nem elérhető — lehet privát, eltávolított vagy régiókorlátozott.",
|
||||
"embedDisabledBody": "A tulajdonos letiltotta a lejátszást más oldalakon (gyakori az auto-generált „Topic” zenei sávoknál). A YouTube-on viszont lejátszható.",
|
||||
"openOnYoutube": "Megnyitás YouTube-on",
|
||||
"shortcutsHint": "Kattintás: lejátszás/szünet · Görgő: hangerő · F: teljes képernyő"
|
||||
"shortcutsHint": "Kattintás: lejátszás/szünet · Görgő: hangerő · F: teljes képernyő",
|
||||
"nativeControls": "YouTube vezérlők aktívak"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue