import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Flipper, Flipped } from "react-flip-toolkit"; import { RiMusic2Line, RiUser3Line, RiAlbumLine } from "@remixicon/react"; import type NodeCG from "@nodecg/types"; import type { CoverArtReplicant, MusicConfigReplicant, MusicReplicant, } from "../types"; const ensureArrayBufferLike = (data: any): ArrayBufferLike => { if (data instanceof Uint8Array || data instanceof ArrayBuffer) { return data.slice(); } else if (Array.isArray(data)) { return new Uint8Array(data); } else if (typeof data === "object" && data !== null) { return new Uint8Array(Object.values(data)); } throw new Error("Unsupported data format"); }; const musicReplicant = nodecg.Replicant( "musicReplicant", ) as unknown as NodeCG.ServerReplicant; const musicConfigReplicant = nodecg.Replicant( "musicConfigReplicant", ) as unknown as NodeCG.ServerReplicant; const coverArtReplicant = nodecg.Replicant( "coverArtReplicant", ) as unknown as NodeCG.ServerReplicant; const useReplicant = ( replicant: NodeCG.ServerReplicant, fn: (newValue: T | undefined, oldValue: T | undefined) => void | (() => void), deps: any[] = [], ) => { useEffect(() => { let cleanup: (() => void) | void; const handler = (newValue: T | undefined, oldValue: T | undefined) => { if (cleanup) cleanup(); // Run previous cleanup before setting new one cleanup = fn(newValue, oldValue); // Store new cleanup function (if provided) }; replicant.on("change", handler); return () => { replicant.off("change", handler); if (cleanup) cleanup(); }; }, [...deps]); }; export function Index() { const [isPlaying, setIsPlaying] = useState(false); const [songInfo, setSongInfo] = useState( {} as Omit, ); useReplicant(musicReplicant, (newValue?: MusicReplicant) => { setIsPlaying(newValue?.playing || false); setSongInfo(newValue || {}); }); const [imageUrl, setImageUrl] = useState(null as null | string); useReplicant( coverArtReplicant, (newValue?: CoverArtReplicant) => { if (newValue?.data && newValue?.format) { const data = ensureArrayBufferLike(newValue.data); // Convert to Blob const blob = new Blob([data], { type: newValue.format, }); const url = URL.createObjectURL(blob); setImageUrl(url); // Free old image return () => { URL.revokeObjectURL(url); }; } }, [], ); const [songAlbumToggle, setSongAlbumToggle] = useState(true); const [showLabelNames, setShowLabelNames] = useState(true); const mainTickerClasses = " bg-black text-white line-clamp-3 min-h-[calc(var(--height)_*_1/3)] py-8 text-8xl "; const subTickerClasses = " min-h-[calc(var(--height)_*_1/5)] bg-[#fdc700] line-clamp-2 py-4 text-5xl "; const topTickerClasses = songAlbumToggle ? mainTickerClasses : subTickerClasses; const bottomTickerClasses = !songAlbumToggle ? mainTickerClasses : subTickerClasses; const [tickerSwapInterval, setTickerSwapInterval] = useState(25000); useReplicant( musicConfigReplicant, (newValue) => { setTickerSwapInterval(newValue!.tickerSwapInterval * 1000); }, [], ); useEffect(() => { let value = songAlbumToggle; let timeout1 = undefined as undefined | NodeJS.Timeout; let timeout2 = undefined as undefined | NodeJS.Timeout; const interval = setInterval(() => { value = !value; setSongAlbumToggle(value); timeout1 = setTimeout(() => { setShowLabelNames(true); timeout2 = setTimeout( () => { setShowLabelNames(false); }, Math.min(tickerSwapInterval * 0.5, 10000), ); }, tickerSwapInterval / 5); }, tickerSwapInterval); return () => { setSongAlbumToggle(true); setShowLabelNames(false); clearInterval(interval); clearTimeout(timeout1); clearTimeout(timeout2); }; }, [tickerSwapInterval, songInfo]); const springOpts = "stiff"; return ( <>
{isPlaying ? (
Godina izdavanja:
{songInfo.year}
) : ( <> )} {imageUrl && isPlaying ? ( Cover art ) : ( <> )}
{isPlaying ? (
{songAlbumToggle ? ( ) : ( )}
{songAlbumToggle ? songInfo.name : songInfo.artist}
{songAlbumToggle ? ( ) : ( )}
{songAlbumToggle ? songInfo.artist : songInfo.album}
) : ( <> )}
); }