import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import axios from "axios"; import { toast } from "sonner"; import useSWR from "swr"; import { Reorder, useDragControls } from "framer-motion"; import { LuCheck, LuGripVertical } from "react-icons/lu"; import { SplitCardRow } from "@/components/card/SettingsGroupCard"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { FrigateConfig } from "@/types/frigateConfig"; import { cn } from "@/lib/utils"; import type { SectionRendererProps } from "./registry"; const SAVED_INDICATOR_MS = 1500; type SaveStatus = "idle" | "saving" | "saved"; export default function BirdseyeCameraReorder({ formContext, }: SectionRendererProps) { const { t } = useTranslation(["views/settings", "common"]); const { data: config, mutate: updateConfig } = useSWR("config"); const birdseyeCameras = useMemo(() => { if (!config) return []; return Object.keys(config.cameras) .filter( (name) => config.cameras[name].enabled_in_config && config.cameras[name].birdseye?.enabled !== false, ) .sort((a, b) => { const orderA = config.cameras[a].birdseye?.order ?? 0; const orderB = config.cameras[b].birdseye?.order ?? 0; if (orderA !== orderB) return orderA - orderB; return a.localeCompare(b); }); }, [config]); const [orderedCameras, setOrderedCameras] = useState(birdseyeCameras); const orderedCamerasRef = useRef(orderedCameras); useEffect(() => { orderedCamerasRef.current = orderedCameras; }, [orderedCameras]); useEffect(() => { setOrderedCameras((prev) => { if ( prev.length === birdseyeCameras.length && prev.every((cam, i) => cam === birdseyeCameras[i]) ) { return prev; } return birdseyeCameras; }); }, [birdseyeCameras]); const [saveStatus, setSaveStatus] = useState("idle"); const savedResetTimerRef = useRef | null>(null); useEffect(() => { return () => { if (savedResetTimerRef.current) { clearTimeout(savedResetTimerRef.current); } }; }, []); const handleDragEnd = useCallback(async () => { const current = orderedCamerasRef.current; if ( current.length === birdseyeCameras.length && current.every((cam, i) => cam === birdseyeCameras[i]) ) { return; } const cameraUpdates: Record = {}; current.forEach((cam, i) => { cameraUpdates[cam] = { birdseye: { order: i * 10 } }; }); if (savedResetTimerRef.current) { clearTimeout(savedResetTimerRef.current); savedResetTimerRef.current = null; } setSaveStatus("saving"); try { await axios.put("config/set", { requires_restart: 0, config_data: { cameras: cameraUpdates }, }); await updateConfig(); setSaveStatus("saved"); savedResetTimerRef.current = setTimeout(() => { setSaveStatus("idle"); savedResetTimerRef.current = null; }, SAVED_INDICATOR_MS); } catch (error) { setOrderedCameras(birdseyeCameras); setSaveStatus("idle"); const errorMessage = axios.isAxiosError(error) && (error.response?.data?.message || error.response?.data?.detail) ? error.response?.data?.message || error.response?.data?.detail : t("toast.save.error.noMessage", { ns: "common" }); toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), { position: "top-center", }); } }, [birdseyeCameras, updateConfig, t]); if (formContext?.level && formContext.level !== "global") { return null; } if (!config || birdseyeCameras.length < 2) { return null; } return ( {orderedCameras.map((camera) => ( ))} } /> ); } type SaveStatusIndicatorProps = { status: SaveStatus; }; function SaveStatusIndicator({ status }: SaveStatusIndicatorProps) { const { t } = useTranslation(["views/settings"]); return (
{status === "saving" && ( {t("birdseye.cameraOrder.saving")} )} {status === "saved" && ( {t("birdseye.cameraOrder.saved")} )}
); } type BirdseyeCameraRowProps = { camera: string; onDragEnd: () => void; }; function BirdseyeCameraRow({ camera, onDragEnd }: BirdseyeCameraRowProps) { const { t } = useTranslation(["views/settings"]); const controls = useDragControls(); return ( ); }