mymusics

retro MySpace-style music player
Log | Files | Refs | README

commit 4cdd32a343ce70f898d7419f498fa4ee76da1dc6
parent 71ad757a42523f92f12dbf1793c009d6f66c717a
Author: Pablo Murad <pblmrd@gmail.com>
Date:   Fri,  1 May 2026 19:17:42 -0300

FIX: player

Diffstat:
Msrc/App.css | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/components/CozyAudioBar.tsx | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/components/EmbedSnippet.tsx | 4++--
Msrc/index.css | 12++++++++++++
Msrc/pages/Embed.tsx | 10++++++++++
5 files changed, 204 insertions(+), 21 deletions(-)

diff --git a/src/App.css b/src/App.css @@ -509,17 +509,97 @@ } .embed-page { - min-height: 100vh; + min-height: 0; margin: 0; - padding: 0.75rem; + padding: 0.45rem; box-sizing: border-box; } .embed-shell { - max-width: 420px; + max-width: 380px; margin: 0 auto; } +.embed-shell .card { + padding: 0.65rem 0.75rem 0.75rem; +} + +.embed-shell .card-head { + margin-bottom: 0.5rem; +} + +.embed-shell .card h2 { + font-size: 1rem; +} + +.embed-shell .track-block .artist { + font-size: 1.08rem; + margin: 0 0 0.2rem; +} + +.embed-shell .track-block .title { + font-size: 0.98rem; + margin: 0 0 0.35rem; +} + +.embed-shell .up-next { + margin-top: 0.45rem; + padding: 0.5rem 0.65rem; +} + +.embed-shell .up-next-label { + font-size: 0.78rem; + margin: 0 0 0.3rem; +} + +.embed-shell .up-next-track { + font-size: 0.84rem; +} + +.embed-shell .player-nook { + margin-top: 0.45rem; + padding: 0.65rem 0.6rem 0.65rem; +} + +.embed-shell .cozy-player { + padding: 0.45rem 0.55rem; + gap: 0.45rem; +} + +.embed-shell .cozy-player__play, +.embed-shell .cozy-player__mute { + width: 2.15rem; + height: 2.15rem; +} + +.embed-shell .cozy-player__icon { + width: 1.15rem; + height: 1.15rem; +} + +.embed-shell .cozy-player__time { + font-size: 0.72rem; +} + +.embed-shell .actions { + margin-top: 0.55rem; + gap: 0.45rem; +} + +.embed-shell .btn.primary { + padding: 0.5rem 1.1rem; + font-size: 0.92rem; +} + +.embed-shell .check { + font-size: 0.76rem; +} + +.embed-shell .hint { + margin: 0.45rem 0 0; + font-size: 0.78rem; +} + .health-banner--embed { margin-bottom: 0.85rem; padding: 0.75rem 0.85rem; diff --git a/src/components/CozyAudioBar.tsx b/src/components/CozyAudioBar.tsx @@ -1,4 +1,4 @@ -import { type ChangeEvent, type RefObject, useCallback, useEffect, useState } from "react"; +import { type FormEvent, type RefObject, useCallback, useEffect, useRef, useState } from "react"; function formatTime(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) return "0:00"; @@ -7,6 +7,30 @@ function formatTime(seconds: number): string { return `${m}:${sec.toString().padStart(2, "0")}`; } +function formatDurationLabel(seconds: number): string { + if (!Number.isFinite(seconds) || seconds <= 0) return "--:--"; + return formatTime(seconds); +} + +/** Duration from element metadata or seekable ranges (streaming MP3). */ +function safeDuration(el: HTMLAudioElement): number { + const d = el.duration; + if (Number.isFinite(d) && d > 0 && d !== Number.POSITIVE_INFINITY) { + return d; + } + try { + if (el.seekable && el.seekable.length > 0) { + const end = el.seekable.end(el.seekable.length - 1); + if (Number.isFinite(end) && end > 0 && end !== Number.POSITIVE_INFINITY) { + return end; + } + } + } catch { + /* ignore */ + } + return 0; +} + type Props = { audioRef: RefObject<HTMLAudioElement | null>; /** No track / no usable stream */ @@ -18,46 +42,88 @@ export function CozyAudioBar({ audioRef, disabled }: Props) { const [duration, setDuration] = useState(0); const [playing, setPlaying] = useState(false); const [muted, setMuted] = useState(false); + const scrubbingRef = useRef(false); useEffect(() => { const el = audioRef.current; if (!el) return; + const syncDuration = () => { + setDuration(safeDuration(el)); + }; + const syncFromElement = () => { - setCurrentTime(el.currentTime); - const d = el.duration; - setDuration(Number.isFinite(d) && d > 0 ? d : 0); + if (!scrubbingRef.current) { + setCurrentTime(el.currentTime); + } + syncDuration(); setPlaying(!el.paused); setMuted(el.muted); }; - const onTimeUpdate = () => setCurrentTime(el.currentTime); + const onTimeUpdate = () => { + if (!scrubbingRef.current) { + setCurrentTime(el.currentTime); + } + }; + const onLoadedMeta = () => syncFromElement(); + const onProgress = () => syncDuration(); + const onLoadedData = () => syncDuration(); + const onCanPlay = () => syncDuration(); + const onSeeked = () => { + scrubbingRef.current = false; + setCurrentTime(el.currentTime); + syncDuration(); + }; + const onSeeking = () => { + setCurrentTime(el.currentTime); + }; const onPlay = () => setPlaying(true); const onPause = () => setPlaying(false); const onVol = () => setMuted(el.muted); el.addEventListener("timeupdate", onTimeUpdate); el.addEventListener("loadedmetadata", onLoadedMeta); - el.addEventListener("durationchange", onLoadedMeta); + el.addEventListener("durationchange", syncDuration); + el.addEventListener("progress", onProgress); + el.addEventListener("loadeddata", onLoadedData); + el.addEventListener("canplay", onCanPlay); + el.addEventListener("seeked", onSeeked); + el.addEventListener("seeking", onSeeking); el.addEventListener("play", onPlay); el.addEventListener("pause", onPause); el.addEventListener("volumechange", onVol); syncFromElement(); + const endScrub = () => { + scrubbingRef.current = false; + setCurrentTime(el.currentTime); + }; + window.addEventListener("pointerup", endScrub); + window.addEventListener("pointercancel", endScrub); + return () => { el.removeEventListener("timeupdate", onTimeUpdate); el.removeEventListener("loadedmetadata", onLoadedMeta); - el.removeEventListener("durationchange", onLoadedMeta); + el.removeEventListener("durationchange", syncDuration); + el.removeEventListener("progress", onProgress); + el.removeEventListener("loadeddata", onLoadedData); + el.removeEventListener("canplay", onCanPlay); + el.removeEventListener("seeked", onSeeked); + el.removeEventListener("seeking", onSeeking); el.removeEventListener("play", onPlay); el.removeEventListener("pause", onPause); el.removeEventListener("volumechange", onVol); + window.removeEventListener("pointerup", endScrub); + window.removeEventListener("pointercancel", endScrub); }; }, [audioRef]); + const dur = duration; const pct = - duration > 0 && Number.isFinite(duration) ? Math.min(100, (currentTime / duration) * 100) : 0; + dur > 0 && Number.isFinite(dur) ? Math.min(100, Math.max(0, (currentTime / dur) * 100)) : 0; const togglePlay = useCallback(() => { const el = audioRef.current; @@ -66,17 +132,29 @@ export function CozyAudioBar({ audioRef, disabled }: Props) { else el.pause(); }, [audioRef, disabled]); - const onSeek = useCallback( - (e: ChangeEvent<HTMLInputElement>) => { + const applySeekPercent = useCallback( + (pctValue: number) => { const el = audioRef.current; - if (!el || disabled || duration <= 0) return; - const next = (parseFloat(e.target.value) / 100) * duration; + if (!el || disabled || dur <= 0) return; + const clamped = Math.min(100, Math.max(0, pctValue)); + const next = (clamped / 100) * dur; el.currentTime = next; setCurrentTime(next); }, - [audioRef, disabled, duration], + [audioRef, disabled, dur], ); + const onSeek = useCallback( + (e: FormEvent<HTMLInputElement>) => { + applySeekPercent(parseFloat(e.currentTarget.value)); + }, + [applySeekPercent], + ); + + const startScrub = useCallback(() => { + scrubbingRef.current = true; + }, []); + const toggleMute = useCallback(() => { const el = audioRef.current; if (!el || disabled) return; @@ -84,8 +162,9 @@ export function CozyAudioBar({ audioRef, disabled }: Props) { setMuted(el.muted); }, [audioRef, disabled]); - const durLabel = formatTime(duration); + const durLabel = formatDurationLabel(dur); const curLabel = formatTime(currentTime); + const seekDisabled = disabled || dur <= 0; return ( <div @@ -119,10 +198,12 @@ export function CozyAudioBar({ audioRef, disabled }: Props) { className="cozy-player__scrub" min={0} max={100} - step={0.25} + step={0.05} value={pct} + onPointerDown={startScrub} + onInput={onSeek} onChange={onSeek} - disabled={disabled || duration <= 0} + disabled={seekDisabled} aria-label="Seek" aria-valuemin={0} aria-valuemax={100} diff --git a/src/components/EmbedSnippet.tsx b/src/components/EmbedSnippet.tsx @@ -7,8 +7,8 @@ function buildIframeSnippet(): string { src="${src}" title="MyMusics" width="100%" - height="420" - style="max-width:420px;border:0;border-radius:12px" + height="500" + style="max-width:380px;border:0;border-radius:12px" loading="lazy" ></iframe>`; } diff --git a/src/index.css b/src/index.css @@ -85,3 +85,15 @@ audio:focus-visible { outline: 2px solid var(--logo-gold); outline-offset: 3px; } + +/* /embed iframe: avoid min-height:100vh forcing scroll inside short iframes */ +html.embed-active, +html.embed-active body, +html.embed-active #root { + min-height: 0 !important; + height: auto; +} + +html.embed-active body { + overflow-x: hidden; +} diff --git a/src/pages/Embed.tsx b/src/pages/Embed.tsx @@ -1,8 +1,18 @@ +import { useEffect } from "react"; import { CozyAudioBar } from "../components/CozyAudioBar"; import { useMyMusicsPlayback } from "../hooks/useMyMusicsPlayback"; import "../App.css"; +const EMBED_ROOT_CLASS = "embed-active"; + export default function Embed() { + useEffect(() => { + document.documentElement.classList.add(EMBED_ROOT_CLASS); + return () => { + document.documentElement.classList.remove(EMBED_ROOT_CLASS); + }; + }, []); + const { audioRef, track,