"use client" import { useEffect, useState, useCallback } from "react" import { Plus, Download, RefreshCw, Settings, X, LogOut } from "lucide-react" import { StreamCard } from "@/components/StreamCard" 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" const CARD_WIDTHS: Record = { mini: "max-w-[200px]", sm: "max-w-[240px]", md: "max-w-[300px]", lg: "max-w-[380px]" } function SortableStreamCard(props: { stream: Stream status?: Record localStatus?: string | null cardSize: CardSize onRefresh: () => void onLocalStatus: (id: string, s: string | null) => void }) { 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: "max-w-[200px]", sm: "max-w-[240px]", md: "max-w-[300px]", lg: "max-w-[380px]" } return (
{[...Array(4)].map((_, i) =>
)}
) } // settings popup function SettingsPopup({ cardSize, onCardSize, onClose }: { cardSize: CardSize onCardSize: (s: CardSize) => void onClose: () => void }) { return ( <>

Settings

Card size

{(["mini", "sm", "md", "lg"] as CardSize[]).map((s) => ( ))}
) } 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 [cardSize, setCardSize] = useState("md") // md = Medium = antigo Big const [settingsOpen, setSettingsOpen] = useState(false) const [authEnabled, setAuthEnabled] = useState(false) 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(() => { const saved = localStorage.getItem("cardSize") as CardSize | null if (saved) setCardSize(saved) }, []) 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 })) }, []) function downloadPlaylist() { window.location.href = `/api/streams/playlist?host=${window.location.hostname}&port=8888` } const showSkeleton = loading || refreshing // all header buttons share the same padding and height 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
{/* explicit h-8 to match other header buttons */} {/* #7 — botão de config */} {authEnabled && ( )}
{/* settings popup */} {settingsOpen && ( { setCardSize(s); localStorage.setItem("cardSize", s); setSettingsOpen(false) }} 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} /> ))}
)}
) }