Merge improvement/player-quality-native-controls: native YT menu access + higher quality (v0.36.0)
This commit is contained in:
commit
a1dfa033a9
6 changed files with 68 additions and 7 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.35.0
|
0.36.0
|
||||||
|
|
@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
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 Avatar from "./Avatar";
|
||||||
import AddToPlaylist from "./AddToPlaylist";
|
import AddToPlaylist from "./AddToPlaylist";
|
||||||
import DownloadButton from "./DownloadButton";
|
import DownloadButton from "./DownloadButton";
|
||||||
|
|
@ -124,6 +124,12 @@ export default function PlayerModal({
|
||||||
const volTimerRef = useRef<number | undefined>(undefined);
|
const volTimerRef = useRef<number | undefined>(undefined);
|
||||||
// Volume level to flash in the on-player overlay (null = hidden). Auto-fades after a moment.
|
// Volume level to flash in the on-player overlay (null = hidden). Auto-fades after a moment.
|
||||||
const [volumeUi, setVolumeUi] = useState<number | null>(null);
|
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 focusModal = () => cardRef.current?.focus();
|
||||||
const togglePlay = () => {
|
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).
|
// Re-attach if the overlay remounts (it's hidden while the embed shows an error).
|
||||||
}, [playerError]);
|
}, [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
|
// Create the player, resume from the saved position, persist progress, and
|
||||||
// auto-mark watched once playback reaches the end.
|
// auto-mark watched once playback reaches the end.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -448,6 +483,11 @@ export default function PlayerModal({
|
||||||
enablejsapi: 1,
|
enablejsapi: 1,
|
||||||
origin: window.location.origin,
|
origin: window.location.origin,
|
||||||
playsinline: 1,
|
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: {
|
events: {
|
||||||
// Keep keyboard focus on the modal, not the iframe, so shortcuts work right away.
|
// 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
|
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
|
the ends. stopPropagation so a click steps instead of closing. Hidden on narrow screens
|
||||||
where there's no room beside the card. */}
|
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 && (
|
{hasQueue && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
@ -552,9 +592,18 @@ export default function PlayerModal({
|
||||||
}}
|
}}
|
||||||
title={t("player.shortcutsHint")}
|
title={t("player.shortcutsHint")}
|
||||||
aria-label={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. */}
|
{/* Volume level flash (auto-fades). pointer-events-none so it never blocks the video. */}
|
||||||
{volumeUi != null && (
|
{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">
|
<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.",
|
"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.",
|
"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",
|
"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.",
|
"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.",
|
"embedDisabledBody": "The owner disabled playback on other sites (common for auto-generated “Topic” music tracks). It still plays on YouTube.",
|
||||||
"openOnYoutube": "Open 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.",
|
"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ó.",
|
"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",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,15 @@ export interface ReleaseEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||||
|
{
|
||||||
|
version: "0.36.0",
|
||||||
|
date: "2026-07-10",
|
||||||
|
summary: "Sharper in-app playback and full access to YouTube's own player menu.",
|
||||||
|
features: [
|
||||||
|
"The in-app player is now larger and asks YouTube for higher quality, so videos look sharper than the old low-resolution default. (For the maximum your connection allows, go fullscreen.)",
|
||||||
|
"You can now open and navigate YouTube's own player menu — Quality, Speed, Captions and More options — all the way down. When you use YouTube's built-in controls, the app's click-to-play and scroll-for-volume shortcuts briefly step aside (a small badge shows this), and they return the moment you move the pointer off the video.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "0.35.0",
|
version: "0.35.0",
|
||||||
date: "2026-07-10",
|
date: "2026-07-10",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue