import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { Button } from "@/components/ui/button"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import useOptimisticState from "@/hooks/use-optimistic-state"; import { isIOS, isMobile } from "react-device-detect"; import { FaVideo } from "react-icons/fa"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; import FilterSwitch from "@/components/filter/FilterSwitch"; import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter"; import { PolygonType } from "@/types/canvas"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import scrollIntoView from "scroll-into-view-if-needed"; import CameraSettingsView from "@/views/settings/CameraSettingsView"; import ObjectSettingsView from "@/views/settings/ObjectSettingsView"; import MotionTunerView from "@/views/settings/MotionTunerView"; import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; import AuthenticationView from "@/views/settings/AuthenticationView"; import NotificationView from "@/views/settings/NotificationsSettingsView"; import EnrichmentsSettingsView from "@/views/settings/EnrichmentsSettingsView"; import UiSettingsView from "@/views/settings/UiSettingsView"; import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView"; import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useSearchParams } from "react-router-dom"; import { useInitialCameraState } from "@/api/ws"; import { isInIframe } from "@/utils/isIFrame"; import { isPWA } from "@/utils/isPWA"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { useTranslation } from "react-i18next"; const allSettingsViews = [ "ui", "enrichments", "cameras", "masksAndZones", "motionTuner", "debug", "users", "notifications", "frigateplus", ] as const; type SettingsType = (typeof allSettingsViews)[number]; export default function Settings() { const { t } = useTranslation(["views/settings"]); const [page, setPage] = useState("ui"); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const tabsRef = useRef(null); const { data: config } = useSWR("config"); const [searchParams] = useSearchParams(); // auth and roles const isAdmin = useIsAdmin(); const allowedViewsForViewer: SettingsType[] = ["ui", "debug"]; const visibleSettingsViews = !isAdmin ? allowedViewsForViewer : allSettingsViews; // TODO: confirm leave page const [unsavedChanges, setUnsavedChanges] = useState(false); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const cameras = useMemo(() => { if (!config) { return []; } return Object.values(config.cameras) .filter((conf) => conf.ui.dashboard && conf.enabled_in_config) .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order); }, [config]); const [selectedCamera, setSelectedCamera] = useState(""); const { payload: allCameraStates } = useInitialCameraState( cameras.length > 0 ? cameras[0].name : "", true, ); const cameraEnabledStates = useMemo(() => { const states: Record = {}; if (allCameraStates) { Object.entries(allCameraStates).forEach(([camName, state]) => { states[camName] = state.config?.enabled ?? false; }); } // fallback to config if ws data isn’t available yet cameras.forEach((cam) => { if (!(cam.name in states)) { states[cam.name] = cam.enabled; } }); return states; }, [allCameraStates, cameras]); const [filterZoneMask, setFilterZoneMask] = useState(); const handleDialog = useCallback( (save: boolean) => { if (unsavedChanges && save) { // TODO } setConfirmationDialogOpen(false); setUnsavedChanges(false); }, [unsavedChanges], ); useEffect(() => { if (cameras.length > 0) { if (!selectedCamera) { // Set to first enabled camera initially if no selection const firstEnabledCamera = cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0]; setSelectedCamera(firstEnabledCamera.name); } else if (!cameraEnabledStates[selectedCamera] && page !== "cameras") { // Switch to first enabled camera if current one is disabled, unless on "camera settings" page const firstEnabledCamera = cameras.find((cam) => cameraEnabledStates[cam.name]) || cameras[0]; if (firstEnabledCamera.name !== selectedCamera) { setSelectedCamera(firstEnabledCamera.name); } } } }, [cameras, selectedCamera, cameraEnabledStates, page]); useEffect(() => { if (tabsRef.current) { const element = tabsRef.current.querySelector( `[data-nav-item="${pageToggle}"]`, ); if (element instanceof HTMLElement) { scrollIntoView(element, { behavior: isMobile && isIOS && !isPWA && isInIframe ? "auto" : "smooth", inline: "start", }); } } }, [tabsRef, pageToggle]); useSearchEffect("page", (page: string) => { if (allSettingsViews.includes(page as SettingsType)) { // Restrict viewer to UI settings if (!isAdmin && !["ui", "debug"].includes(page)) { setPage("ui"); } else { setPage(page as SettingsType); } } // don't clear url params if we're creating a new object mask return !searchParams.has("object_mask"); }); useSearchEffect("camera", (camera: string) => { const cameraNames = cameras.map((c) => c.name); if (cameraNames.includes(camera)) { setSelectedCamera(camera); } // don't clear url params if we're creating a new object mask return !searchParams.has("object_mask"); }); useEffect(() => { document.title = t("documentTitle.default"); }, [t]); return (
{ if (value) { // Restrict viewer navigation if (!isAdmin && !["ui", "debug"].includes(value)) { setPageToggle("ui"); } else { setPageToggle(value); } } }} > {visibleSettingsViews.map((item) => (
{t("menu." + item)}
))}
{(page == "debug" || page == "cameras" || page == "masksAndZones" || page == "motionTuner") && (
{page == "masksAndZones" && ( )}
)}
{page == "ui" && } {page == "enrichments" && ( )} {page == "debug" && ( )} {page == "cameras" && ( )} {page == "masksAndZones" && ( )} {page == "motionTuner" && ( )} {page == "users" && } {page == "notifications" && ( )} {page == "frigateplus" && ( )}
{confirmationDialogOpen && ( setConfirmationDialogOpen(false)} > {t("dialog.unsavedChanges.title")} {t("dialog.unsavedChanges.desc")} handleDialog(false)}> {t("button.cancel", { ns: "common" })} handleDialog(true)}> {t("button.save", { ns: "common" })} )}
); } type CameraSelectButtonProps = { allCameras: CameraConfig[]; selectedCamera: string; setSelectedCamera: React.Dispatch>; cameraEnabledStates: Record; currentPage: SettingsType; }; function CameraSelectButton({ allCameras, selectedCamera, setSelectedCamera, cameraEnabledStates, currentPage, }: CameraSelectButtonProps) { const { t } = useTranslation(["views/settings"]); const [open, setOpen] = useState(false); if (!allCameras.length) { return null; } const trigger = ( ); const content = ( <> {isMobile && ( <> {t("cameraSetting.camera")} )}
{allCameras.map((item) => { const isEnabled = cameraEnabledStates[item.name]; const isCameraSettingsPage = currentPage === "cameras"; return ( { if (isChecked && (isEnabled || isCameraSettingsPage)) { setSelectedCamera(item.name); setOpen(false); } }} disabled={!isEnabled && !isCameraSettingsPage} /> ); })}
); if (isMobile) { return ( { if (!open) { setSelectedCamera(selectedCamera); } setOpen(open); }} > {trigger} {content} ); } return ( { if (!open) { setSelectedCamera(selectedCamera); } setOpen(open); }} > {trigger} {content} ); }