Merge: promote dev to prod

This commit is contained in:
npeter83 2026-07-10 15:54:24 +02:00
commit ac11ff2f01
6 changed files with 68 additions and 7 deletions

View file

@ -1 +1 @@
0.35.0 0.36.0

View file

@ -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">

View file

@ -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"
} }

View file

@ -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"
} }

View file

@ -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"
} }

View file

@ -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",