code.pablomurad.com

mymusics

Retro web player for random tracks from the Myspace Dragon Hoard

src/hooks/useEmbedMessaging.ts raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { useCallback, useEffect } from "react";

import type { TrackInfo } from "./useMyMusicsPlayback";
import {
  parseThemePayload,
  resolveEmbedTheme,
  tokensToCssVars,
  type EmbedThemeOverrides,
} from "../lib/embedTheme";

export type EmbedPlaybackState = "playing" | "paused" | "buffering" | "error";

const PARENT_ORIGIN =
  typeof import.meta.env.VITE_EMBED_PARENT_ORIGIN === "string" &&
  import.meta.env.VITE_EMBED_PARENT_ORIGIN.trim()
    ? import.meta.env.VITE_EMBED_PARENT_ORIGIN.trim()
    : "*";

function post(type: string, payload: Record<string, unknown> = {}) {
  if (typeof window === "undefined" || window.parent === window) return;
  window.parent.postMessage({ source: "mymusics", type, ...payload }, PARENT_ORIGIN);
}

type Options = {
  enabled: boolean;
  trackCount: number | null;
  track: TrackInfo | null;
  streamUrl: string | null;
  playbackState: EmbedPlaybackState;
  onNext: () => void;
  onPlay: () => void;
  onPause: () => void;
  onTheme?: (patch: EmbedThemeOverrides) => void;
  themeOverrides?: EmbedThemeOverrides;
};

export function useEmbedMessaging({
  enabled,
  trackCount,
  track,
  streamUrl,
  playbackState,
  onNext,
  onPlay,
  onPause,
  onTheme,
  themeOverrides,
}: Options) {
  useEffect(() => {
    if (!enabled) return;
    post("mymusics:ready", { trackCount });
  }, [enabled, trackCount]);

  useEffect(() => {
    if (!enabled || !track) return;
    post("mymusics:track", {
      id: track.id,
      title: track.title,
      artist: track.artist,
      streamUrl,
    });
  }, [enabled, track, streamUrl]);

  useEffect(() => {
    if (!enabled) return;
    post("mymusics:state", { state: playbackState });
  }, [enabled, playbackState]);

  useEffect(() => {
    if (!enabled) return;
    const tokens = resolveEmbedTheme(themeOverrides ?? {});
    post("mymusics:theme-applied", { tokens: tokensToCssVars(tokens) });
  }, [enabled, themeOverrides]);

  const postError = useCallback(
    (code: string, message: string) => {
      if (!enabled) return;
      post("mymusics:error", { code, message });
    },
    [enabled],
  );

  useEffect(() => {
    if (!enabled) return;
    const onMessage = (ev: MessageEvent) => {
      const data = ev.data as {
        source?: string;
        type?: string;
        command?: string;
        theme?: unknown;
      };
      if (data?.source !== "mymusics-host") return;
      if (PARENT_ORIGIN !== "*" && ev.origin !== PARENT_ORIGIN) return;

      if (data.type === "mymusics:theme") {
        const patch = parseThemePayload(data.theme);
        if (patch && onTheme) onTheme(patch);
        return;
      }

      if (data.type !== "mymusics:command") return;
      const cmd = data.command;
      if (cmd === "play") onPlay();
      else if (cmd === "pause") onPause();
      else if (cmd === "next") onNext();
    };
    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, [enabled, onNext, onPlay, onPause, onTheme]);

  return { postError };
}