diff --git a/README.md b/README.md index da0c374..629d2c5 100644 --- a/README.md +++ b/README.md @@ -1501,14 +1501,17 @@ Things I chose while building, with the reasoning: while Aero is on and the listener has not turned it off — light/dark request nothing from soundcloud.com, and a suite check pins that; the off choice persists (`freezmo.aero-music.v1`) while re-selecting Aero in Settings is fresh consent (`applyTheme` clears the flag); volume - starts at 45; playback pauses rather than resumes after a reload (browsers require a gesture, - so the circle comes up paused and one tap starts it); the iframe is 1px and inert, only the - audio engine; the control clears the phone tab bar, lifts above the chat composer and the + starts at 45; the iframe is 1px and inert, only the audio engine; the control clears the phone tab bar, lifts above the chat composer and the wizard bar, steps aside under immersive overlays and while the cookie notice shares the corner, and the credit area collapses back to the circle without stopping the music. The cookie policy gained the one third-party-embed paragraph it was owed (soundcloud.com may set its own cookies; Freezmo still sets none). `tests/aero-music-test.mjs` stubs the widget API - and pins all of it (21 checks). + and pins all of it (28 checks). **Follow-up:** it must not need a tap on the player at all. The + embed URL now carries `auto_play=true` as a second try, and when the browser still refuses + (a reload has no gesture) the retry rides the next `pointerdown`/`keydown` anywhere *outside* + the player — so the first tap on anything starts the music, with a 700ms throttle so a flurry + of taps cannot skip through the set. An explicit pause or the × is respected: no later gesture + undoes it. The suite simulates the refusal (`__scAllowPlay`) and pins all three paths. 100. **The review breakdown carries each deck's own total.** The follow-up to 99: "bro its not possible, I only have 2 decks" — the screenshot showed three deck cards (one clipped at the diff --git a/components/AeroMusic.tsx b/components/AeroMusic.tsx index a012a14..1d55826 100644 --- a/components/AeroMusic.tsx +++ b/components/AeroMusic.tsx @@ -84,6 +84,11 @@ export function AeroMusic() { const [allowed, setAllowed] = useState(false); const [expanded, setExpanded] = useState(false); const [playing, setPlaying] = useState(false); + /** True until playback is actually running (or the listener said no): a + * blocked autoplay is retried from the next real gesture, so the theme + * still "just plays" without a dedicated tap on the player. */ + const [autoStart, setAutoStart] = useState(true); + const startAttempts = useRef(0); const [ready, setReady] = useState(false); const [track, setTrack] = useState<{ title: string; artist: string } | null>(null); @@ -116,6 +121,15 @@ export function AeroMusic() { [refreshTrack], ); + /** The retry path: called from the next gesture, throttled so a flurry of + * taps does not skip through the playlist. */ + const startFromGesture = useCallback(() => { + const now = Date.now(); + if (now - startAttempts.current < 700) return; + startAttempts.current = now; + playRandom(); + }, [playRandom]); + useEffect(() => { if (!show) return; const element = iframe.current; @@ -133,14 +147,16 @@ export function AeroMusic() { instance.getSounds((sounds) => { if (cancelled) return; trackCount.current = sounds.length; - /* A gesture selected the theme, so this usually plays right away; - if the browser refuses (a reload with no gesture), the circle - stays paused until the listener taps it. */ + /* Tried at once, with no gesture: allowed when the theme was just + picked (that click counts) and wherever the browser trusts the + site. When it is refused, the next tap or key press starts the + music instead (see the gesture effect below). */ playRandom(); }); }); instance.bind(PLAY, () => { setPlaying(true); + setAutoStart(false); refreshTrack(); }); instance.bind(PAUSE, () => setPlaying(false)); @@ -153,6 +169,24 @@ export function AeroMusic() { }; }, [show, playRandom, refreshTrack]); + /* Autoplay was refused: the next real gesture anywhere outside the player + starts the music. Taps on the player's own controls are left alone — they + carry their own intent (expand, play, turn it off). */ + useEffect(() => { + if (!show || !autoStart || playing) return; + const onGesture = (event: Event) => { + const target = event.target; + if (target instanceof Element && target.closest("[data-testid=aero-music]")) return; + startFromGesture(); + }; + document.addEventListener("pointerdown", onGesture, true); + document.addEventListener("keydown", onGesture, true); + return () => { + document.removeEventListener("pointerdown", onGesture, true); + document.removeEventListener("keydown", onGesture, true); + }; + }, [show, autoStart, playing, startFromGesture]); + if (!show) return null; const title = track?.title ?? FALLBACK_TITLE; @@ -173,7 +207,7 @@ export function AeroMusic() { allow="autoplay; encrypted-media" scrolling="no" frameBorder="no" - src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(PLAYLIST_URL)}&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false&show_teaser=false&visual=false&color=%2338bdf8`} + src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(PLAYLIST_URL)}&auto_play=true&hide_related=true&show_comments=false&show_user=true&show_reposts=false&show_teaser=false&visual=false&color=%2338bdf8`} className="pointer-events-none absolute size-px opacity-0" /> @@ -187,6 +221,8 @@ export function AeroMusic() {