diff --git a/web/src/views/settings/UiSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx index 6e5f4d3b2..11950935c 100644 --- a/web/src/views/settings/UiSettingsView.tsx +++ b/web/src/views/settings/UiSettingsView.tsx @@ -1,10 +1,7 @@ -import Heading from "@/components/ui/heading"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; -import { useCallback, useContext, useEffect } from "react"; -import { Toaster } from "@/components/ui/sonner"; -import { toast } from "sonner"; -import { Separator } from "../../components/ui/separator"; +import { ReactNode, useCallback, useContext, useEffect } from "react"; +import { Toaster, toast } from "sonner"; import { Button } from "../../components/ui/button"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; @@ -25,6 +22,100 @@ import { AuthContext } from "@/context/auth-context"; const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; const WEEK_STARTS_ON = ["Sunday", "Monday"]; +const SPLIT_ROW_CLASS_NAME = + "space-y-2 md:grid md:grid-cols-[minmax(14rem,22rem)_minmax(0,1fr)] md:items-start md:gap-x-6 md:space-y-0"; +const DESCRIPTION_CLASS_NAME = "text-sm text-muted-foreground"; +const CONTROL_COLUMN_CLASS_NAME = "w-full md:max-w-2xl"; + +type SettingsGroupCardProps = { + title: string; + children: ReactNode; +}; + +function SettingsGroupCard({ title, children }: SettingsGroupCardProps) { + return ( +
+
+ {title} +
+ {children} +
+ ); +} + +type SwitchSettingRowProps = { + id: string; + label: string; + description: string; + checked: boolean | undefined; + onCheckedChange: (checked: boolean | undefined) => void; +}; + +function SwitchSettingRow({ + id, + label, + description, + checked, + onCheckedChange, +}: SwitchSettingRowProps) { + return ( +
+
+
+ +
+ +
+
+

{description}

+
+
+ +
+
+ ); +} + +type ValueSettingRowProps = { + id: string; + label: string; + description: string; + control: ReactNode; +}; + +function ValueSettingRow({ + id, + label, + description, + control, +}: ValueSettingRowProps) { + return ( +
+
+ +

+ {description} +

+
+
+ {control} +

{description}

+
+
+ ); +} export default function UiSettingsView() { const { data: config } = useSWR("config"); @@ -37,13 +128,11 @@ export default function UiSettingsView() { return []; } - Object.entries(config.camera_groups).forEach(async (value) => { - await deleteUserNamespacedKey(`${value[0]}-draggable-layout`, username) + Object.entries(config.camera_groups).forEach(async ([cameraName]) => { + await deleteUserNamespacedKey(`${cameraName}-draggable-layout`, username) .then(() => { toast.success( - t("general.toast.success.clearStoredLayout", { - cameraName: value[0], - }), + t("general.toast.success.clearStoredLayout", { cameraName }), { position: "top-center", }, @@ -69,7 +158,7 @@ export default function UiSettingsView() { return []; } - await deleteUserNamespacedKey(`streaming-settings`, username) + await deleteUserNamespacedKey("streaming-settings", username) .then(() => { toast.success(t("general.toast.success.clearStreamingSettings"), { position: "top-center", @@ -95,8 +184,6 @@ export default function UiSettingsView() { document.title = t("documentTitle.general"); }, [t]); - // settings - const [autoLive, setAutoLive] = useUserPersistence("autoLiveView", true); const [cameraNames, setCameraName] = useUserPersistence( "displayCameraNames", @@ -110,229 +197,195 @@ export default function UiSettingsView() { 3, ); + const liveDashboardSwitchRows = [ + { + id: "auto-live", + label: t("general.liveDashboard.automaticLiveView.label"), + description: t("general.liveDashboard.automaticLiveView.desc"), + checked: autoLive, + onCheckedChange: setAutoLive, + }, + { + id: "images-only", + label: t("general.liveDashboard.playAlertVideos.label"), + description: t("general.liveDashboard.playAlertVideos.desc"), + checked: alertVideos, + onCheckedChange: setAlertVideos, + }, + { + id: "camera-names", + label: t("general.liveDashboard.displayCameraNames.label"), + description: t("general.liveDashboard.displayCameraNames.desc"), + checked: cameraNames, + onCheckedChange: setCameraName, + }, + ]; + return ( - <> -
- -
- - {t("general.title")} - +
+ +
+
+ +
+ {liveDashboardSwitchRows.map((row) => ( + + ))} - - - - {t("general.liveDashboard.title")} - - -
-
-
- - -
-
-

{t("general.liveDashboard.automaticLiveView.desc")}

-
-
-
-
- - -
-
-

{t("general.liveDashboard.playAlertVideos.desc")}

-
-
-
-
- - -
-
-

{t("general.liveDashboard.displayCameraNames.desc")}

-
-
-
-
- -
-
-

{t("general.liveDashboard.liveFallbackTimeout.desc")}

-
- -
-
- -
-
-
-
- {t("general.storedLayouts.title")} -
-
-

{t("general.storedLayouts.desc")}

-
-
- -
- -
-
-
- {t("general.cameraGroupStreaming.title")} -
-
-

{t("general.cameraGroupStreaming.desc")}

-
-
- -
- - - - - {t("general.recordingsViewer.title")} - - -
-
-
- {t("general.recordingsViewer.defaultPlaybackRate.label")} -
-
-

- {t("general.recordingsViewer.defaultPlaybackRate.desc")} -

-
-
-
- - - - - {t("general.calendar.title")} - - -
-
-
- {t("general.calendar.firstWeekday.label")} -
-
-

{t("general.calendar.firstWeekday.desc")}

-
-
-
- - -
+ {t("time.second", { + ns: "common", + time: fallbackTimeout, + count: fallbackTimeout, + })} + + + + {[1, 2, 3, 5, 8, 10, 12, 15].map((timeout) => ( + + {t("time.second", { + ns: "common", + time: timeout, + count: timeout, + })} + + ))} + + + + } + /> + + + {t("general.storedLayouts.clearAll")} + + } + /> + + + {t("general.cameraGroupStreaming.clearAll")} + + } + /> +
+
+ + + setPlaybackRate(parseFloat(value))} + > + + {`${playbackRate}x`} + + + + {PLAYBACK_RATE_DEFAULT.map((rate) => ( + + {rate}x + + ))} + + + + } + /> + + + + + setWeekStartsOn(parseInt(value, 10)) + } + > + + {t( + "general.calendar.firstWeekday." + + WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(), + )} + + + + {WEEK_STARTS_ON.map((day, index) => ( + + {t( + "general.calendar.firstWeekday." + + day.toLowerCase(), + )} + + ))} + + + + } + /> +
- +
); }