diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index fbdff3200..af1d47f9c 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -17,6 +17,7 @@ import { cn } from "@/lib/utils"; type LivePlayerProps = { cameraRef?: (ref: HTMLDivElement | null) => void; + containerRef?: React.MutableRefObject; className?: string; cameraConfig: CameraConfig; preferredLiveMode?: LivePlayerMode; @@ -26,13 +27,14 @@ type LivePlayerProps = { micEnabled?: boolean; // only webrtc supports mic iOSCompatFullScreen?: boolean; pip?: boolean; + autoLive?: boolean; onClick?: () => void; setFullResolution?: React.Dispatch>; - containerRef?: React.MutableRefObject; }; export default function LivePlayer({ cameraRef = undefined, + containerRef, className, cameraConfig, preferredLiveMode, @@ -42,9 +44,9 @@ export default function LivePlayer({ micEnabled = false, iOSCompatFullScreen = false, pip, + autoLive = true, onClick, setFullResolution, - containerRef, }: LivePlayerProps) { // camera activity @@ -64,6 +66,10 @@ export default function LivePlayer({ const [liveReady, setLiveReady] = useState(false); useEffect(() => { + if (!autoLive) { + return; + } + if (!liveReady) { if (cameraActive && liveMode == "jsmpeg") { setLiveReady(true); @@ -77,7 +83,7 @@ export default function LivePlayer({ } // live mode won't change // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cameraActive, liveReady]); + }, [autoLive, cameraActive, liveReady]); // camera still state @@ -91,18 +97,31 @@ export default function LivePlayer({ } if (activeMotion || activeTracking) { - return 200; + if (autoLive) { + return 200; + } else { + return 59000; + } } return 30000; - }, [liveReady, activeMotion, activeTracking, offline, windowVisible]); + }, [ + autoLive, + liveReady, + activeMotion, + activeTracking, + offline, + windowVisible, + ]); if (!cameraConfig) { return ; } let player; - if (liveMode == "webrtc") { + if (!autoLive) { + player = null; + } else if (liveMode == "webrtc") { player = (
- {!offline && activeMotion && ( + {autoLive && !offline && activeMotion && ( )} {offline && ( diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx index fad130ecd..b6f501d73 100644 --- a/web/src/components/settings/MotionMaskEditPane.tsx +++ b/web/src/components/settings/MotionMaskEditPane.tsx @@ -131,7 +131,7 @@ export default function MotionMaskEditPane({ axios .put(`config/set?${queryString}`, { - requires_restart: 0, + requires_restart: 1, }) .then((res) => { if (res.status === 200) { diff --git a/web/src/components/settings/ObjectMaskEditPane.tsx b/web/src/components/settings/ObjectMaskEditPane.tsx index cd0963b7c..885a6adfb 100644 --- a/web/src/components/settings/ObjectMaskEditPane.tsx +++ b/web/src/components/settings/ObjectMaskEditPane.tsx @@ -189,7 +189,7 @@ export default function ObjectMaskEditPane({ axios .put(`config/set?${queryString}`, { - requires_restart: 0, + requires_restart: 1, }) .then((res) => { if (res.status === 200) { diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index 5512e7d3a..a0b436224 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -197,7 +197,7 @@ export default function ZoneEditPane({ await axios.put( `config/set?cameras.${polygon.camera}.zones.${polygon.name}${renameAlertQueries}${renameDetectionQueries}`, { - requires_restart: 0, + requires_restart: 1, }, ); @@ -257,7 +257,7 @@ export default function ZoneEditPane({ axios .put( `config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${objectQueries}${alertQueries}${detectionQueries}`, - { requires_restart: 0 }, + { requires_restart: 1 }, ) .then((res) => { if (res.status === 200) { diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 1141763ef..d2239f647 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -17,8 +17,6 @@ import { } from "@/components/ui/alert-dialog"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; -import MotionTuner from "@/components/settings/MotionTuner"; -import MasksAndZones from "@/components/settings/MasksAndZones"; import { Button } from "@/components/ui/button"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import useOptimisticState from "@/hooks/use-optimistic-state"; @@ -26,14 +24,16 @@ import { isMobile } from "react-device-detect"; import { FaVideo } from "react-icons/fa"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import useSWR from "swr"; -import General from "@/components/settings/General"; import FilterSwitch from "@/components/filter/FilterSwitch"; import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter"; import { PolygonType } from "@/types/canvas"; -import ObjectSettings from "@/components/settings/ObjectSettings"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import scrollIntoView from "scroll-into-view-if-needed"; -import Authentication from "@/components/settings/Authentication"; +import GeneralSettingsView from "@/views/settings/GeneralSettingsView"; +import ObjectSettingsView from "@/views/settings/ObjectSettingsView"; +import MotionTunerView from "@/views/settings/MotionTunerView"; +import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; +import AuthenticationView from "@/views/settings/AuthenticationView"; export default function Settings() { const settingsViews = [ @@ -156,22 +156,24 @@ export default function Settings() { )}
- {page == "general" && } - {page == "debug" && } + {page == "general" && } + {page == "debug" && ( + + )} {page == "masks / zones" && ( - )} {page == "motion tuner" && ( - )} - {page == "users" && } + {page == "users" && }
{confirmationDialogOpen && ( (null); // recent events + const { payload: eventUpdate } = useFrigateReviews(); const { data: allEvents, mutate: updateEvents } = useSWR([ "review", @@ -92,6 +93,7 @@ export default function LiveDashboardView({ // camera live views + const [autoLiveView] = usePersistence("autoLiveView", true); const [windowVisible, setWindowVisible] = useState(true); const visibilityListener = useCallback(() => { setWindowVisible(document.visibilityState == "visible"); @@ -261,6 +263,7 @@ export default function LiveDashboardView({ } cameraConfig={camera} preferredLiveMode={isSafari ? "webrtc" : "mse"} + autoLive={autoLiveView} onClick={() => onSelectCamera(camera.name)} /> ); diff --git a/web/src/components/settings/Authentication.tsx b/web/src/views/settings/AuthenticationView.tsx similarity index 92% rename from web/src/components/settings/Authentication.tsx rename to web/src/views/settings/AuthenticationView.tsx index 0d28957fc..64fe9dc5b 100644 --- a/web/src/components/settings/Authentication.tsx +++ b/web/src/views/settings/AuthenticationView.tsx @@ -3,20 +3,20 @@ import ActivityIndicator from "@/components/indicators/activity-indicator"; import { FrigateConfig } from "@/types/frigateConfig"; import { Toaster } from "@/components/ui/sonner"; import useSWR from "swr"; -import Heading from "../ui/heading"; +import Heading from "@/components/ui/heading"; import { User } from "@/types/user"; -import { Button } from "../ui/button"; -import SetPasswordDialog from "../overlay/SetPasswordDialog"; +import { Button } from "@/components/ui/button"; +import SetPasswordDialog from "@/components/overlay/SetPasswordDialog"; import axios from "axios"; -import CreateUserDialog from "../overlay/CreateUserDialog"; +import CreateUserDialog from "@/components/overlay/CreateUserDialog"; import { toast } from "sonner"; -import DeleteUserDialog from "../overlay/DeleteUserDialog"; -import { Card } from "../ui/card"; +import DeleteUserDialog from "@/components/overlay/DeleteUserDialog"; +import { Card } from "@/components/ui/card"; import { HiTrash } from "react-icons/hi"; import { FaUserEdit } from "react-icons/fa"; import { LuPlus } from "react-icons/lu"; -export default function Authentication() { +export default function AuthenticationView() { const { data: config } = useSWR("config"); const { data: users, mutate: mutateUsers } = useSWR("users"); diff --git a/web/src/components/settings/General.tsx b/web/src/views/settings/GeneralSettingsView.tsx similarity index 74% rename from web/src/components/settings/General.tsx rename to web/src/views/settings/GeneralSettingsView.tsx index 31510cd8e..3e145382c 100644 --- a/web/src/components/settings/General.tsx +++ b/web/src/views/settings/GeneralSettingsView.tsx @@ -4,8 +4,8 @@ import { Switch } from "@/components/ui/switch"; import { useCallback, useEffect } from "react"; import { Toaster } from "sonner"; import { toast } from "sonner"; -import { Separator } from "../ui/separator"; -import { Button } from "../ui/button"; +import { Separator } from "../../components/ui/separator"; +import { Button } from "../../components/ui/button"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { del as delData } from "idb-keyval"; @@ -17,11 +17,11 @@ import { SelectGroup, SelectItem, SelectTrigger, -} from "../ui/select"; +} from "../../components/ui/select"; const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; -export default function General() { +export default function GeneralSettingsView() { const { data: config } = useSWR("config"); const clearStoredLayouts = useCallback(() => { @@ -49,6 +49,9 @@ export default function General() { document.title = "General Settings - Frigate"; }, []); + // settings + + const [autoLive, setAutoLive] = usePersistence("autoLiveView", true); const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1); return ( @@ -60,7 +63,35 @@ export default function General() { General Settings -
+ + + + Live Dashboard + + +
+
+
+ + +
+
+

+ Automatically switch to a camera's live view when activity is + detected. Disabling this option causes static camera images on + the Live dashboard to only update once per minute. +

+
+
+
+ +
Stored Layouts
@@ -72,11 +103,15 @@ export default function General() {

-
- -
+
+ + + + Recordings Viewer + +
Default Playback Rate
@@ -107,26 +142,6 @@ export default function General() { -
-
-
Low Data Mode
-
-

- Not yet implemented. Default: disabled -

-
-
-
- {}} - /> - -
-
diff --git a/web/src/components/settings/MasksAndZones.tsx b/web/src/views/settings/MasksAndZonesView.tsx similarity index 97% rename from web/src/components/settings/MasksAndZones.tsx rename to web/src/views/settings/MasksAndZonesView.tsx index 628f99d5e..09e8343ac 100644 --- a/web/src/components/settings/MasksAndZones.tsx +++ b/web/src/views/settings/MasksAndZonesView.tsx @@ -9,10 +9,10 @@ import { useRef, useState, } from "react"; -import { PolygonCanvas } from "./PolygonCanvas"; +import { PolygonCanvas } from "@/components/settings/PolygonCanvas"; import { Polygon, PolygonType } from "@/types/canvas"; import { interpolatePoints, parseCoordinates } from "@/utils/canvasUtil"; -import { Skeleton } from "../ui/skeleton"; +import { Skeleton } from "@/components/ui/skeleton"; import { useResizeObserver } from "@/hooks/resize-observer"; import { LuExternalLink, LuPlus } from "react-icons/lu"; import { @@ -22,29 +22,33 @@ import { } from "@/components/ui/hover-card"; import copy from "copy-to-clipboard"; import { toast } from "sonner"; -import { Toaster } from "../ui/sonner"; -import { Button } from "../ui/button"; -import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; -import Heading from "../ui/heading"; -import ZoneEditPane from "./ZoneEditPane"; -import MotionMaskEditPane from "./MotionMaskEditPane"; -import ObjectMaskEditPane from "./ObjectMaskEditPane"; -import PolygonItem from "./PolygonItem"; +import { Toaster } from "@/components/ui/sonner"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import Heading from "@/components/ui/heading"; +import ZoneEditPane from "@/components/settings/ZoneEditPane"; +import MotionMaskEditPane from "@/components/settings/MotionMaskEditPane"; +import ObjectMaskEditPane from "@/components/settings/ObjectMaskEditPane"; +import PolygonItem from "@/components/settings/PolygonItem"; import { Link } from "react-router-dom"; import { isDesktop } from "react-device-detect"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; -type MasksAndZoneProps = { +type MasksAndZoneViewProps = { selectedCamera: string; selectedZoneMask?: PolygonType[]; setUnsavedChanges: React.Dispatch>; }; -export default function MasksAndZones({ +export default function MasksAndZonesView({ selectedCamera, selectedZoneMask, setUnsavedChanges, -}: MasksAndZoneProps) { +}: MasksAndZoneViewProps) { const { data: config } = useSWR("config"); const [allPolygons, setAllPolygons] = useState([]); const [editingPolygons, setEditingPolygons] = useState([]); diff --git a/web/src/components/settings/MotionTuner.tsx b/web/src/views/settings/MotionTunerView.tsx similarity index 97% rename from web/src/components/settings/MotionTuner.tsx rename to web/src/views/settings/MotionTunerView.tsx index ad149de54..0c36eb4a0 100644 --- a/web/src/components/settings/MotionTuner.tsx +++ b/web/src/views/settings/MotionTunerView.tsx @@ -12,17 +12,17 @@ import { useMotionContourArea, useMotionThreshold, } from "@/api/ws"; -import { Skeleton } from "../ui/skeleton"; -import { Button } from "../ui/button"; -import { Switch } from "../ui/switch"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; -import { Separator } from "../ui/separator"; +import { Separator } from "@/components/ui/separator"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; -type MotionTunerProps = { +type MotionTunerViewProps = { selectedCamera: string; setUnsavedChanges: React.Dispatch>; }; @@ -33,10 +33,10 @@ type MotionSettings = { improve_contrast?: boolean; }; -export default function MotionTuner({ +export default function MotionTunerView({ selectedCamera, setUnsavedChanges, -}: MotionTunerProps) { +}: MotionTunerViewProps) { const { data: config, mutate: updateConfig } = useSWR("config"); const [changedValue, setChangedValue] = useState(false); @@ -113,7 +113,7 @@ export default function MotionTuner({ axios .put( `config/set?cameras.${selectedCamera}.motion.threshold=${motionSettings.threshold}&cameras.${selectedCamera}.motion.contour_area=${motionSettings.contour_area}&cameras.${selectedCamera}.motion.improve_contrast=${motionSettings.improve_contrast}`, - { requires_restart: 0 }, + { requires_restart: 1 }, ) .then((res) => { if (res.status === 200) { diff --git a/web/src/components/settings/ObjectSettings.tsx b/web/src/views/settings/ObjectSettingsView.tsx similarity index 96% rename from web/src/components/settings/ObjectSettings.tsx rename to web/src/views/settings/ObjectSettingsView.tsx index 3054acb5f..4ec43f3e5 100644 --- a/web/src/components/settings/ObjectSettings.tsx +++ b/web/src/views/settings/ObjectSettingsView.tsx @@ -5,19 +5,19 @@ import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import { Toaster } from "@/components/ui/sonner"; import { Label } from "@/components/ui/label"; import useSWR from "swr"; -import Heading from "../ui/heading"; -import { Switch } from "../ui/switch"; +import Heading from "@/components/ui/heading"; +import { Switch } from "@/components/ui/switch"; import { usePersistence } from "@/hooks/use-persistence"; -import { Skeleton } from "../ui/skeleton"; +import { Skeleton } from "@/components/ui/skeleton"; import { useCameraActivity } from "@/hooks/use-camera-activity"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ObjectType } from "@/types/ws"; import useDeepMemo from "@/hooks/use-deep-memo"; -import { Card } from "../ui/card"; +import { Card } from "@/components/ui/card"; import { getIconForLabel } from "@/utils/iconUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; -type ObjectSettingsProps = { +type ObjectSettingsViewProps = { selectedCamera?: string; }; @@ -25,9 +25,9 @@ type Options = { [key: string]: boolean }; const emptyObject = Object.freeze({}); -export default function ObjectSettings({ +export default function ObjectSettingsView({ selectedCamera, -}: ObjectSettingsProps) { +}: ObjectSettingsViewProps) { const { data: config } = useSWR("config"); const DEBUG_OPTIONS = [