From ad00001049c11057465dcf0d745a91f5b2a31427 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Thu, 12 Feb 2026 14:45:12 -0600
Subject: [PATCH] refactor profile settings to match rjsf forms
---
web/src/views/settings/UiSettingsView.tsx | 515 ++++++++++++----------
1 file changed, 284 insertions(+), 231 deletions(-)
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 (
+
+ );
+}
+
+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(),
+ )}
+
+ ))}
+
+
+
+ }
+ />
+
- >
+
);
}