mymusics

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

useEmbedMessaging.ts (3037B)


      1 import { useCallback, useEffect } from "react";
      2 
      3 import type { TrackInfo } from "./useMyMusicsPlayback";
      4 import {
      5   parseThemePayload,
      6   resolveEmbedTheme,
      7   tokensToCssVars,
      8   type EmbedThemeOverrides,
      9 } from "../lib/embedTheme";
     10 
     11 export type EmbedPlaybackState = "playing" | "paused" | "buffering" | "error";
     12 
     13 const PARENT_ORIGIN =
     14   typeof import.meta.env.VITE_EMBED_PARENT_ORIGIN === "string" &&
     15   import.meta.env.VITE_EMBED_PARENT_ORIGIN.trim()
     16     ? import.meta.env.VITE_EMBED_PARENT_ORIGIN.trim()
     17     : "*";
     18 
     19 function post(type: string, payload: Record<string, unknown> = {}) {
     20   if (typeof window === "undefined" || window.parent === window) return;
     21   window.parent.postMessage({ source: "mymusics", type, ...payload }, PARENT_ORIGIN);
     22 }
     23 
     24 type Options = {
     25   enabled: boolean;
     26   trackCount: number | null;
     27   track: TrackInfo | null;
     28   streamUrl: string | null;
     29   playbackState: EmbedPlaybackState;
     30   onNext: () => void;
     31   onPlay: () => void;
     32   onPause: () => void;
     33   onTheme?: (patch: EmbedThemeOverrides) => void;
     34   themeOverrides?: EmbedThemeOverrides;
     35 };
     36 
     37 export function useEmbedMessaging({
     38   enabled,
     39   trackCount,
     40   track,
     41   streamUrl,
     42   playbackState,
     43   onNext,
     44   onPlay,
     45   onPause,
     46   onTheme,
     47   themeOverrides,
     48 }: Options) {
     49   useEffect(() => {
     50     if (!enabled) return;
     51     post("mymusics:ready", { trackCount });
     52   }, [enabled, trackCount]);
     53 
     54   useEffect(() => {
     55     if (!enabled || !track) return;
     56     post("mymusics:track", {
     57       id: track.id,
     58       title: track.title,
     59       artist: track.artist,
     60       streamUrl,
     61     });
     62   }, [enabled, track, streamUrl]);
     63 
     64   useEffect(() => {
     65     if (!enabled) return;
     66     post("mymusics:state", { state: playbackState });
     67   }, [enabled, playbackState]);
     68 
     69   useEffect(() => {
     70     if (!enabled) return;
     71     const tokens = resolveEmbedTheme(themeOverrides ?? {});
     72     post("mymusics:theme-applied", { tokens: tokensToCssVars(tokens) });
     73   }, [enabled, themeOverrides]);
     74 
     75   const postError = useCallback(
     76     (code: string, message: string) => {
     77       if (!enabled) return;
     78       post("mymusics:error", { code, message });
     79     },
     80     [enabled],
     81   );
     82 
     83   useEffect(() => {
     84     if (!enabled) return;
     85     const onMessage = (ev: MessageEvent) => {
     86       const data = ev.data as {
     87         source?: string;
     88         type?: string;
     89         command?: string;
     90         theme?: unknown;
     91       };
     92       if (data?.source !== "mymusics-host") return;
     93       if (PARENT_ORIGIN !== "*" && ev.origin !== PARENT_ORIGIN) return;
     94 
     95       if (data.type === "mymusics:theme") {
     96         const patch = parseThemePayload(data.theme);
     97         if (patch && onTheme) onTheme(patch);
     98         return;
     99       }
    100 
    101       if (data.type !== "mymusics:command") return;
    102       const cmd = data.command;
    103       if (cmd === "play") onPlay();
    104       else if (cmd === "pause") onPause();
    105       else if (cmd === "next") onNext();
    106     };
    107     window.addEventListener("message", onMessage);
    108     return () => window.removeEventListener("message", onMessage);
    109   }, [enabled, onNext, onPlay, onPause, onTheme]);
    110 
    111   return { postError };
    112 }