Files
music-overlay/src/graphics/Index.tsx
T
2025-02-20 12:39:27 +01:00

267 lines
8.4 KiB
TypeScript

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<MusicReplicant>;
const musicConfigReplicant = nodecg.Replicant(
"musicConfigReplicant",
) as unknown as NodeCG.ServerReplicant<MusicConfigReplicant>;
const coverArtReplicant = nodecg.Replicant(
"coverArtReplicant",
) as unknown as NodeCG.ServerReplicant<CoverArtReplicant>;
const useReplicant = <T = any,>(
replicant: NodeCG.ServerReplicant<T>,
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<MusicReplicant, "playing">,
);
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 (
<>
<div
className="p-16 overflow-hidden drop-shadow-2xl"
style={{
display: "flex",
...{ "--height": "26vw" },
}}
>
<div className={"flex flex-col relative "}>
{isPlaying ? (
<div
className={
"flex bg-[#00a63e] text-3xl p-3 text-white relative transition "
}
style={{
transform: `translateY(${songAlbumToggle ? 100 : 0}%)`,
}}
>
<div className="">Godina izdavanja:</div>
<div className="grow" />
<div className="font-bold">{songInfo.year}</div>
</div>
) : (
<></>
)}
{imageUrl && isPlaying ? (
<img
className={"z-10"}
src={imageUrl}
alt="Cover art"
style={{
width: "var(--height)",
height: "var(--height)",
minWidth: "var(--height)",
minHeight: "var(--height)",
}}
/>
) : (
<></>
)}
</div>
{isPlaying ? (
<div className="block relative flex flex-col">
<div className="grow" />
<Flipper
flipKey={
songAlbumToggle + "," + showLabelNames + "," + songInfo.name
}
spring={springOpts}
>
<Flipped flipId="tickers">
<div
className={
"inline-flex relative z-30 overflow-visible shrink-0 transition-colors duration-100 flex-col justify-center min-w-(--height) px-8 font-bold " +
topTickerClasses +
"" ||
(showLabelNames && !songAlbumToggle ? " pb-12 pt-8 " : "")
}
>
<div
className={
"absolute flex items-center justify-center transition origin-left overflow-hidden min-h-[calc(var(--height)_/_8)] min-w-[calc(var(--height)_/_8)] text-2xl top-[calc(100%_-_var(--height)_/_10.66)] right-[calc(var(--height)_/_-10.66)] font-bold bg-[#00a63e] h-[var(--height)_/_10.66] text-white top-4 z-20 ml-4 max-w-fit "
}
style={{
transform: showLabelNames ? " scaleX(1) " : " scaleX(0) ",
}}
>
{songAlbumToggle ? (
<RiMusic2Line color="white" size={"2.5rem"} />
) : (
<RiUser3Line color="white" size={"2.5rem"} />
)}
</div>
{songAlbumToggle ? songInfo.name : songInfo.artist}
</div>
</Flipped>
</Flipper>
<Flipper flipKey={songAlbumToggle} spring={springOpts}>
<Flipped flipId="tickers">
<div
className={
"inline-flex overflow-visible relative shrink-0 transition-colors duration-100 flex-col justify-center shrink-0 min-w-(--height) px-8 font-bold z-20 " +
bottomTickerClasses
}
>
<div
className={
"absolute flex items-center justify-center transition origin-left overflow-hidden min-h-[calc(var(--height)_/_8)] min-w-[calc(var(--height)_/_8)] text-2xl top-[calc(100%_-_var(--height)_/_10.66)] right-[calc(var(--height)_/_-10.66)] font-bold bg-[#00a63e] h-[var(--height)_/_10.66] text-white top-4 ml-4 max-w-fit "
}
style={{
transform: showLabelNames ? " scaleX(1) " : " scaleX(0) ",
}}
>
{songAlbumToggle ? (
<RiUser3Line color="white" size={"2.5rem"} />
) : (
<RiAlbumLine color="white" size={"2.5rem"} />
)}
</div>
{songAlbumToggle ? songInfo.artist : songInfo.album}
</div>
</Flipped>
</Flipper>
</div>
) : (
<></>
)}
</div>
</>
);
}