Added spotify and moved away from hardcoded urls
Some checks failed
Build App / build (push) Has been cancelled

This commit is contained in:
2026-03-01 18:30:31 +01:00
parent d29a2a565d
commit 6f66342dea
15 changed files with 516 additions and 27 deletions

View File

@@ -121,7 +121,7 @@ function App() {
if (screenStatus === "fullscreen") {
const handlePullState = () => {
fetch(
"https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329/pull_full",
`${import.meta.env.VITE_INSTANCE_URL}/pull_full`,
)
.then((response) => response.json())
.then((data) => {

View File

@@ -5,6 +5,7 @@ import type {
CustomJsonCard,
DataCard,
ImageRotatorCard,
SpotifyCard,
StaticTextCard,
} from "../types";
import { evaluatePath } from "../utils/evaluatePath";
@@ -304,12 +305,151 @@ function ImageRotatorWidget({ card, isNightMode, nightMessage }: { card: ImageRo
);
}
// ─── spotify widget ──────────────────────────────────────────────────────────
interface SpotifyItem {
name: string;
duration_ms: number;
artists: Array<{ name: string }>;
album: {
name: string;
images: Array<{ url: string; width: number; height: number }>;
};
}
interface SpotifyPayload {
is_playing: boolean;
progress_ms?: number;
item?: SpotifyItem;
}
function SpotifyWidget({ card }: { card: SpotifyCard }) {
const [payload, setPayload] = useState<SpotifyPayload | null>(null);
const [error, setError] = useState<string | null>(null);
const [pulling, setPulling] = useState(false);
const [dots, setDots] = useState(0);
// live progress ticker — resets to 0 on each successful fetch
const [tickMs, setTickMs] = useState(0);
// Advance local progress every second
useEffect(() => {
const timer = setInterval(() => setTickMs((t) => t + 1000), 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
if (!pulling) return;
const d = setInterval(() => setDots((x) => (x + 1) % 4), 400);
return () => clearInterval(d);
}, [pulling]);
useEffect(() => {
const doFetch = () => {
setPulling(true);
setDots(0);
fetch(card.config.url)
.then((r) => r.json())
.then((raw) => {
// Support both bare payload and { status, data } wrapper
const p: SpotifyPayload = raw?.data ?? raw;
setPayload(p);
setTickMs(0);
setError(null);
setPulling(false);
})
.catch((e) => {
setError(String(e));
setPulling(false);
});
};
doFetch();
const ms = Math.max(5000, (card.config.refresh_interval ?? 30) * 1000);
const iv = setInterval(doFetch, ms);
return () => clearInterval(iv);
}, [card.id, card.config.url, card.config.refresh_interval]);
const item = payload?.item;
const isPlaying = payload?.is_playing ?? false;
const progressMs = Math.min(
item?.duration_ms ?? 0,
(payload?.progress_ms ?? 0) + (isPlaying ? tickMs : 0),
);
const durationMs = item?.duration_ms ?? 0;
const progressPct = durationMs > 0 ? (progressMs / durationMs) * 100 : 0;
const albumArt = item?.album.images?.[0]?.url;
const trackName = item?.name ?? "";
const artistNames = item?.artists?.map((a) => a.name).join(", ") ?? "";
const albumName = item?.album?.name ?? "";
const fmtMs = (ms: number) => {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
return `${m}:${String(s % 60).padStart(2, "0")}`;
};
return (
<CardShell
name={card.name}
footer={
pulling ? (
<span className="text-gray-500 text-xs">syncing{".".repeat(dots)}</span>
) : undefined
}
>
{error ? (
<span className="text-red-400 text-sm break-all"> {error}</span>
) : !payload ? (
<span className="text-gray-500 text-sm">Loading</span>
) : !isPlaying || !item ? (
<div className="flex flex-col items-center justify-center h-full gap-2 opacity-50">
<span className="text-4xl"></span>
<span className="text-gray-400 text-sm">Nothing playing</span>
</div>
) : (
<div className="flex flex-col h-full gap-2 min-h-0">
{/* Album art + info row */}
<div className="flex gap-3 items-center min-h-0 flex-1 overflow-hidden">
{albumArt && (
<img
src={albumArt}
alt={albumName}
className="rounded-lg shrink-0 object-cover"
style={{ width: 56, height: 56 }}
/>
)}
<div className="flex flex-col justify-center overflow-hidden">
<span className="text-white font-semibold text-sm leading-tight truncate">{trackName}</span>
<span className="text-gray-400 text-xs truncate">{artistNames}</span>
<span className="text-gray-600 text-xs truncate mt-0.5">{albumName}</span>
</div>
</div>
{/* Progress bar */}
<div className="shrink-0">
<div className="w-full h-1 rounded-full bg-white/10">
<div
className="h-1 rounded-full bg-green-500"
style={{ width: `${progressPct}%`, transition: "width 1s linear" }}
/>
</div>
<div className="flex justify-between mt-0.5">
<span className="text-gray-600 text-xs tabular-nums">{fmtMs(progressMs)}</span>
<span className="text-gray-600 text-xs tabular-nums">{fmtMs(durationMs)}</span>
</div>
</div>
</div>
)}
</CardShell>
);
}
// ─── Dispatcher ───────────────────────────────────────────────────────────────
export function DataCardWidget({ card, layout, isNightMode, nightMessage }: { card: DataCard; layout?: CardLayout; isNightMode?: boolean; nightMessage?: string }) {
if (card.type === "static_text") return <StaticTextWidget card={card} layout={layout} />;
if (card.type === "clock") return <ClockWidget card={card} layout={layout} />;
if (card.type === "image_rotator") return <ImageRotatorWidget card={card} isNightMode={isNightMode} nightMessage={nightMessage} />;
if (card.type === "spotify") return <SpotifyWidget card={card} />;
// default: custom_json
return <CustomJsonWidget card={card as CustomJsonCard} layout={layout} />;
}

View File

@@ -117,6 +117,22 @@ export interface ImageRotatorCard {
layout?: CardLayout;
}
// ─── spotify ──────────────────────────────────────────────────────────────────
export interface SpotifyConfig {
/** URL of a Spotify currently-playing proxy that returns the standard shape */
url: string;
refresh_interval: number;
}
export interface SpotifyCard {
id: string;
type: "spotify";
name: string;
config: SpotifyConfig;
layout?: CardLayout;
}
// ─── Union ────────────────────────────────────────────────────────────────────
export type DataCard = CustomJsonCard | StaticTextCard | ClockCard | ImageRotatorCard;
export type DataCard = CustomJsonCard | StaticTextCard | ClockCard | ImageRotatorCard | SpotifyCard;

View File

@@ -4,4 +4,5 @@ import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
envDir: '../',
})