"use client" import { useEffect, useState, useCallback, useRef } from "react" import { Plus, Download, RefreshCw, Settings, X, LogOut } from "lucide-react" import { cn } from "@/lib/utils" import { StreamCard } from "@/components/StreamCard" import { Toggle } from "@/components/Toggle" import type { Stream } from "@/types/stream" import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from "@dnd-kit/core" import type { DragEndEvent } from "@dnd-kit/core" import { SortableContext, useSortable, rectSortingStrategy, arrayMove } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" type CardSize = "mini" | "sm" | "md" | "lg" type GlobalPrefs = { pureMode: boolean; newTab: boolean; autoReload: boolean; reloadInterval: number } const DEFAULT_GLOBAL_PREFS: GlobalPrefs = { pureMode: false, newTab: false, autoReload: false, reloadInterval: 2 } const CARD_WIDTHS: Record = { mini: "sm:max-w-[200px]", sm: "sm:max-w-[240px]", md: "sm:max-w-[300px]", lg: "sm:max-w-[380px]" } function SortableStreamCard(props: { stream: Stream status?: Record localStatus?: string | null cardSize: CardSize onRefresh: () => void onLocalStatus: (id: string, s: string | null) => void globalPrefs: GlobalPrefs }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: props.stream.id }) const style = { transform: CSS.Transform.toString(transform), transition } return (
) } function SkeletonCard({ size = "sm" }: { size?: CardSize }) { const widths = { mini: "sm:max-w-[200px]", sm: "sm:max-w-[240px]", md: "sm:max-w-[300px]", lg: "sm:max-w-[380px]" } return (
{[...Array(4)].map((_, i) =>
)}
) } function SettingsPopup({ cardSize, onCardSize, globalPrefs, onGlobalPrefs, authEnabled, onDownloadPlaylist, onLogout, onClose }: { cardSize: CardSize onCardSize: (s: CardSize) => void globalPrefs: GlobalPrefs onGlobalPrefs: (patch: Partial) => void authEnabled: boolean onDownloadPlaylist: () => void onLogout: () => void onClose: () => void }) { const toggleRow = "flex items-center gap-2 w-full px-1 py-1.5 rounded hover:bg-[#2a2a2a] transition-colors cursor-pointer text-sm" return ( <>

Settings

Card size

{(["mini", "sm", "md", "lg"] as CardSize[]).map((s) => ( ))}

Cards

Player

{globalPrefs.autoReload && (
Interval onGlobalPrefs({ reloadInterval: Math.max(1, Number(e.target.value) || 1) })} className="w-14 px-2 py-1 text-xs rounded border border-border bg-muted text-center" /> min
)}
{authEnabled && ( )}
) } export default function GalleryPage() { const [streams, setStreams] = useState([]) const [statuses, setStatuses] = useState>>({}) const [localStatuses, setLocalStatuses] = useState>({}) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [spinningFab, setSpinningFab] = useState(false) const spinTimerRef = useRef | null>(null) const [cardSize, setCardSize] = useState("md") const [settingsOpen, setSettingsOpen] = useState(false) const [authEnabled, setAuthEnabled] = useState(false) const [globalPrefs, setGlobalPrefs] = useState(DEFAULT_GLOBAL_PREFS) const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })) async function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!over || active.id === over.id) return const oldIndex = streams.findIndex((s) => s.id === active.id) const newIndex = streams.findIndex((s) => s.id === over.id) const reordered = arrayMove(streams, oldIndex, newIndex) setStreams(reordered) await fetch("/api/streams/reorder", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: reordered.map((s) => s.id) }), }) } useEffect(() => { try { const savedSize = localStorage.getItem("cardSize") as CardSize | null if (savedSize) setCardSize(savedSize) const savedPrefs = localStorage.getItem("global-prefs") if (savedPrefs) setGlobalPrefs({ ...DEFAULT_GLOBAL_PREFS, ...JSON.parse(savedPrefs) }) } catch {} }, []) function updateGlobalPrefs(patch: Partial) { setGlobalPrefs(prev => { const next = { ...prev, ...patch } localStorage.setItem("global-prefs", JSON.stringify(next)) return next }) } const fetchStreams = useCallback(async (manual = false) => { if (manual) setRefreshing(true) const res = await fetch("/api/streams") const data: Stream[] = await res.json() setStreams(data) setLoading(false) if (manual) setRefreshing(false) }, []) const fetchStatuses = useCallback(async (list: Stream[]) => { if (list.length === 0) return const res = await fetch("/api/streams/statuses") const data: Record> = await res.json() setStatuses(data) }, []) useEffect(() => { fetchStreams() fetch("/api/auth/status").then(r => r.json()).then(d => setAuthEnabled(d.enabled)) }, [fetchStreams]) useEffect(() => { if (streams.length === 0) return fetchStatuses(streams) const interval = setInterval(() => fetchStatuses(streams), 10000) return () => clearInterval(interval) }, [streams, fetchStatuses]) const setLocalStatus = useCallback((id: string, s: string | null) => { setLocalStatuses((prev) => ({ ...prev, [id]: s })) }, []) async function handleFabRefresh() { if (spinTimerRef.current) clearTimeout(spinTimerRef.current) setSpinningFab(true) const t0 = Date.now() await fetchStreams(true) const remaining = Math.max(0, 1000 - (Date.now() - t0)) spinTimerRef.current = setTimeout(() => setSpinningFab(false), remaining) } function downloadPlaylist() { window.location.href = `/api/streams/playlist?host=${window.location.hostname}&port=8888` } const showSkeleton = loading || refreshing const btnBase = "flex items-center gap-1.5 text-sm px-3 py-1.5 h-8 rounded border border-border hover:bg-[#2a2a2a] active:bg-[#333] transition-colors cursor-pointer" const btnPrimary = "flex items-center gap-1.5 text-sm px-3 py-1.5 h-8 rounded border border-primary bg-primary text-primary-foreground hover:bg-[#2a2a2a] hover:text-foreground hover:border-border active:bg-[#333] transition-colors cursor-pointer" return (
Decap Stream

Decap Stream

ยท riguetto.dev
{settingsOpen && ( { setCardSize(s); localStorage.setItem("cardSize", s) }} globalPrefs={globalPrefs} onGlobalPrefs={updateGlobalPrefs} authEnabled={authEnabled} onDownloadPlaylist={downloadPlaylist} onLogout={async () => { await fetch("/api/auth/logout", { method: "POST" }); window.location.href = "/login" }} onClose={() => setSettingsOpen(false)} /> )}
{showSkeleton ? (
{[...Array(refreshing ? Math.max(streams.length, 1) : 4)].map((_, i) => ( ))}
) : streams.length === 0 ? (

No streams configured.

) : ( s.id)} strategy={rectSortingStrategy}>
{streams.map((s) => ( fetchStreams()} onLocalStatus={setLocalStatus} globalPrefs={globalPrefs} /> ))}
)}
) }