Embed.tsx (6166B)
1 import { useCallback, useEffect, useMemo, useState } from "react"; 2 import { useLocation } from "react-router-dom"; 3 4 import { CozyAudioBar } from "../components/CozyAudioBar"; 5 import { PlayerAttribution } from "../components/PlayerAttribution"; 6 import { PlayerStatus } from "../components/PlayerStatus"; 7 import { PUBLIC_SITE_URL } from "../config/siteUrl"; 8 import { parseEmbedParams } from "../lib/embedParams"; 9 import { 10 applyEmbedTheme, 11 clearEmbedTheme, 12 mergeThemeOverrides, 13 type EmbedThemeOverrides, 14 } from "../lib/embedTheme"; 15 import { useEmbedMessaging } from "../hooks/useEmbedMessaging"; 16 import { useMyMusicsPlayback } from "../hooks/useMyMusicsPlayback"; 17 import "../App.css"; 18 19 const EMBED_ROOT_CLASS = "embed-active"; 20 21 export default function Embed() { 22 const location = useLocation(); 23 const params = useMemo(() => parseEmbedParams(location.search), [location.search]); 24 const [runtimePatches, setRuntimePatches] = useState<Record<string, EmbedThemeOverrides>>({}); 25 26 const themeOverrides = useMemo( 27 () => mergeThemeOverrides(params.themeOverrides, runtimePatches[location.search] ?? {}), 28 [params.themeOverrides, runtimePatches, location.search], 29 ); 30 31 useEffect(() => { 32 document.documentElement.classList.add(EMBED_ROOT_CLASS); 33 return () => { 34 document.documentElement.classList.remove(EMBED_ROOT_CLASS); 35 clearEmbedTheme(); 36 }; 37 }, []); 38 39 useEffect(() => { 40 applyEmbedTheme(themeOverrides); 41 }, [themeOverrides]); 42 43 const handleThemePatch = useCallback( 44 (patch: EmbedThemeOverrides) => { 45 setRuntimePatches((prev) => ({ 46 ...prev, 47 [location.search]: mergeThemeOverrides(prev[location.search] ?? {}, patch), 48 })); 49 }, 50 [location.search], 51 ); 52 53 const { 54 audioRef, 55 preloadAudioRef, 56 track, 57 streamUrl, 58 upNext, 59 status, 60 playbackPhase, 61 embedPlaybackState, 62 autoPlay, 63 setAutoPlay, 64 healthWarn, 65 poolTrackCount, 66 queueBusy, 67 requestNextTrack, 68 handleAudioPlaying, 69 handleAudioError, 70 handleAudioPause, 71 handlePlay, 72 handlePause, 73 onEnded, 74 showUpNextHint, 75 } = useMyMusicsPlayback({ 76 startTrackId: params.startId, 77 autoplayOnMount: params.autoplay, 78 autoAdvance: params.autoplay, 79 startMuted: params.startMuted, 80 }); 81 82 useEmbedMessaging({ 83 enabled: true, 84 trackCount: poolTrackCount, 85 track, 86 streamUrl, 87 playbackState: embedPlaybackState, 88 onNext: requestNextTrack, 89 onPlay: handlePlay, 90 onPause: handlePause, 91 onTheme: handleThemePatch, 92 themeOverrides, 93 }); 94 95 const shellClass = 96 params.theme === "compact" ? "embed-shell embed-shell--compact" : "embed-shell"; 97 98 return ( 99 <div className="embed-page"> 100 <div className={shellClass}> 101 {healthWarn ? ( 102 <details className="health-banner health-banner--embed"> 103 <summary>Server metadata</summary> 104 <p>{healthWarn}</p> 105 </details> 106 ) : null} 107 108 <article className="card now-playing"> 109 <header className="card-head"> 110 <h2>Now playing</h2> 111 </header> 112 {track ? ( 113 <div className="track-block"> 114 <p className="artist">{track.artist}</p> 115 <p className="title">{track.title}</p> 116 </div> 117 ) : ( 118 <p className="muted">{status || "No track loaded."}</p> 119 )} 120 121 <section className="up-next" aria-label="Up next"> 122 <h3 className="up-next-label">Up next</h3> 123 {upNext ? ( 124 <> 125 <p className="up-next-track"> 126 <span className="up-next-artist">{upNext.artist}</span> 127 <span className="up-next-sep"> — </span> 128 <span className="up-next-title">{upNext.title}</span> 129 </p> 130 {showUpNextHint ? ( 131 <p className="up-next-note muted">Only one track — repeats.</p> 132 ) : null} 133 </> 134 ) : queueBusy ? ( 135 <p className="up-next-empty muted">Queuing…</p> 136 ) : track ? ( 137 <p className="up-next-empty muted">—</p> 138 ) : ( 139 <p className="up-next-empty muted">Queuing…</p> 140 )} 141 </section> 142 143 <div className="player-nook"> 144 <audio 145 ref={audioRef} 146 className="audio-hidden" 147 preload="metadata" 148 tabIndex={-1} 149 aria-hidden="true" 150 onEnded={onEnded} 151 onPlaying={handleAudioPlaying} 152 onPause={handleAudioPause} 153 onError={handleAudioError} 154 /> 155 <audio ref={preloadAudioRef} className="audio-hidden" preload="auto" aria-hidden="true" /> 156 <CozyAudioBar audioRef={audioRef} disabled={!track} /> 157 <PlayerStatus phase={playbackPhase} status={status} hasTrack={!!track} compact /> 158 159 <div className="actions"> 160 <button type="button" className="btn primary" onClick={() => void requestNextTrack()}> 161 Next 162 </button> 163 {params.autoplay ? ( 164 <label className="check"> 165 <input 166 type="checkbox" 167 checked={autoPlay} 168 onChange={(e) => setAutoPlay(e.target.checked)} 169 /> 170 Auto-advance 171 </label> 172 ) : null} 173 </div> 174 </div> 175 <PlayerAttribution compact /> 176 </article> 177 178 {params.showBrand ? ( 179 <div className="embed-brand"> 180 <a 181 className="embed-brand-link" 182 href={PUBLIC_SITE_URL} 183 target="_blank" 184 rel="noopener noreferrer" 185 title="MyMusics" 186 > 187 <img 188 className="embed-brand-logo" 189 src="/mymusics.png" 190 alt="MyMusics" 191 width={200} 192 height={80} 193 decoding="async" 194 /> 195 </a> 196 </div> 197 ) : null} 198 </div> 199 </div> 200 ); 201 }