commit 4cdd32a343ce70f898d7419f498fa4ee76da1dc6
parent 71ad757a42523f92f12dbf1793c009d6f66c717a
Author: Pablo Murad <pblmrd@gmail.com>
Date: Fri, 1 May 2026 19:17:42 -0300
FIX: player
Diffstat:
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,