import { useContext, useEffect, useRef, useState } from "react"; import { Link } from "react-router"; import { Trans } from "@lingui/react/macro"; import { t } from "@lingui/core/macro"; import { PlayerContext, type PlayerItem, playerItemKey, } from "../contexts/PlayerContext.ts"; import { MediaPlayer } from "./MediaPlayer.tsx"; import { fmt } from "../utils/duration.ts"; import Thumbnail from "./Thumbnail.tsx"; type EmbedItem = Extract; // The stored embedUrl is the canonical, non-playing form — it is also rendered // outside the player, so the autoplay parameter is added here at playback time // rather than baked into rich_content. Only a fresh play() sets autoplay: a // session restored from localStorage has no user gesture behind it, so it stays // paused, matching how MediaPlayer treats file items. // Bandcamp's EmbeddedPlayer has no autoplay parameter — that is what the native // playback path (GERBEUR_BANDCAMP_PLAYER=native) exists to solve. function playbackUrl(item: EmbedItem, autoplay: boolean) { if (!autoplay) return item.embedUrl; try { const url = new URL(item.embedUrl); if (item.type === "youtube") url.searchParams.set("autoplay", "1"); else if (item.type === "soundcloud") { url.searchParams.set("auto_play", "true"); } else return item.embedUrl; return url.toString(); } catch { return item.embedUrl; // malformed stored URL — hand it to the iframe as-is } } export function GlobalPlayer() { const { current, queue, queueIndex, hasNext, hasPrevious, resolving, startTime, autoplay, stop, next, previous, playAt, seekRef, toggleRef, onPlayStateChange, onTimeUpdate, onEnded, onError, } = useContext(PlayerContext); const ref = useRef(null); const currentRowRef = useRef(null); const [reduced, setReduced] = useState(false); const [prevKey, setPrevKey] = useState(current ? playerItemKey(current) : null); const currentKey = current ? playerItemKey(current) : null; if (prevKey !== currentKey) { setPrevKey(currentKey); if (current) setReduced(false); } useEffect(() => { if (!current) { document.body.classList.remove("has-player"); document.body.style.removeProperty("--player-height"); return; } const el = ref.current; if (!el) return; document.body.style.setProperty("--player-height", `${el.offsetHeight}px`); document.body.classList.add("has-player"); const observer = new ResizeObserver(() => { document.body.style.setProperty( "--player-height", `${el.offsetHeight}px`, ); }); observer.observe(el); return () => { observer.disconnect(); document.body.classList.remove("has-player"); document.body.style.removeProperty("--player-height"); }; }, [current]); // Keep the playing row visible as the queue advances on its own. useEffect(() => { currentRowRef.current?.scrollIntoView({ block: "nearest" }); }, [queueIndex]); if (!current) return null; // Files are classed by their media kind; everything else carries a brand key, // so a native Bandcamp stream keeps the same styling as the Bandcamp embed. const typeClass = current.kind === "file" ? (current.mimeType.startsWith("video/") ? "file-video" : "file-audio") : current.type; const title = current.title ?? (current.kind === "embed" ? current.embedUrl : current.kind === "file" ? current.fileUrl : current.streamUrl); const showQueue = queue.length > 1; return (
{current.artworkUrl && ( )}
{current.dumpHref ? ( {title} ) : {title}} {current.subtitle && ( {current.subtitle} )}
{showQueue && (
{queueIndex + 1} / {queue.length}
)}
{current.kind === "embed" ? (