mymusics

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

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 }