diff --git a/frigate/api/app.py b/frigate/api/app.py
index f4f513e14..6fdedab90 100644
--- a/frigate/api/app.py
+++ b/frigate/api/app.py
@@ -292,7 +292,7 @@ def config_set():
f.close()
# Validate the config schema
try:
- FrigateConfig.parse_raw(new_raw_config)
+ config_obj = FrigateConfig.parse_raw(new_raw_config)
except Exception:
with open(config_file, "w") as f:
f.write(old_raw_config)
@@ -314,6 +314,13 @@ def config_set():
500,
)
+ json = request.get_json(silent=True) or {}
+
+ if json.get("requires_restart", 1) == 0:
+ current_app.frigate_config = FrigateConfig.runtime_config(
+ config_obj, current_app.plus_api
+ )
+
return make_response(
jsonify(
{
diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py
index 30388251d..aa009aa04 100644
--- a/frigate/util/builtin.py
+++ b/frigate/util/builtin.py
@@ -204,17 +204,22 @@ def update_yaml_from_url(file_path, url):
key_path.pop(i - 1)
except ValueError:
pass
- new_value = new_value_list[0]
- update_yaml_file(file_path, key_path, new_value)
+
+ if len(new_value_list) > 1:
+ update_yaml_file(file_path, key_path, new_value_list)
+ else:
+ update_yaml_file(file_path, key_path, new_value_list[0])
def update_yaml_file(file_path, key_path, new_value):
yaml = YAML()
+ yaml.indent(mapping=2, sequence=4, offset=2)
with open(file_path, "r") as f:
data = yaml.load(f)
data = update_yaml(data, key_path, new_value)
-
+ with open("/config/test.yaml", "w") as f:
+ yaml.dump(data, f)
with open(file_path, "w") as f:
yaml.dump(data, f)
diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx
index f975d290c..61414281a 100644
--- a/web/src/components/filter/CameraGroupSelector.tsx
+++ b/web/src/components/filter/CameraGroupSelector.tsx
@@ -1,4 +1,8 @@
-import { FrigateConfig } from "@/types/frigateConfig";
+import {
+ CameraGroupConfig,
+ FrigateConfig,
+ GROUP_ICONS,
+} from "@/types/frigateConfig";
import { isDesktop } from "react-device-detect";
import useSWR from "swr";
import { MdHome } from "react-icons/md";
@@ -8,6 +12,19 @@ import { useNavigate } from "react-router-dom";
import { useCallback, useMemo, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { getIconForGroup } from "@/utils/iconUtil";
+import { LuPencil, LuPlus, LuTrash } from "react-icons/lu";
+import { Dialog, DialogContent, DialogTitle } from "../ui/dialog";
+import { Input } from "../ui/input";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "../ui/dropdown-menu";
+import FilterCheckBox from "./FilterCheckBox";
+import axios from "axios";
type CameraGroupSelectorProps = {
className?: string;
@@ -49,10 +66,20 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
);
}, [config]);
+ // add group
+
+ const [addGroup, setAddGroup] = useState(false);
+
return (
+
+
);
})}
+ {isDesktop && (
+
+ )}
);
}
+
+type NewGroupDialogProps = {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ currentGroups: [string, CameraGroupConfig][];
+};
+function NewGroupDialog({ open, setOpen, currentGroups }: NewGroupDialogProps) {
+ const { data: config, mutate: updateConfig } =
+ useSWR("config");
+
+ // add fields
+
+ const [editState, setEditState] = useState<"none" | "add" | "edit">("none");
+ const [newTitle, setNewTitle] = useState("");
+ const [icon, setIcon] = useState("");
+ const [cameras, setCameras] = useState([]);
+
+ // validation
+
+ const [error, setError] = useState("");
+
+ const onCreateGroup = useCallback(async () => {
+ if (!newTitle) {
+ setError("A title must be selected");
+ return;
+ }
+
+ if (!icon) {
+ setError("An icon must be selected");
+ return;
+ }
+
+ if (!cameras || cameras.length < 2) {
+ setError("At least 2 cameras must be selected");
+ return;
+ }
+
+ setError("");
+ const orderQuery = `camera_groups.${newTitle}.order=${currentGroups.length}`;
+ const iconQuery = `camera_groups.${newTitle}.icon=${icon}`;
+ const cameraQueries = cameras
+ .map((cam) => `&camera_groups.${newTitle}.cameras=${cam}`)
+ .join("");
+
+ const req = axios.put(
+ `config/set?${orderQuery}&${iconQuery}${cameraQueries}`,
+ { requires_restart: 0 },
+ );
+
+ setOpen(false);
+
+ if ((await req).status == 200) {
+ setNewTitle("");
+ setIcon("");
+ setCameras([]);
+ updateConfig();
+ }
+ }, [currentGroups, cameras, newTitle, icon, setOpen, updateConfig]);
+
+ const onDeleteGroup = useCallback(
+ async (name: string) => {
+ const req = axios.put(`config/set?camera_groups.${name}`, {
+ requires_restart: 0,
+ });
+
+ if ((await req).status == 200) {
+ updateConfig();
+ }
+ },
+ [updateConfig],
+ );
+
+ return (
+
+ );
+}
diff --git a/web/src/components/filter/FilterCheckBox.tsx b/web/src/components/filter/FilterCheckBox.tsx
new file mode 100644
index 000000000..b23a4e42c
--- /dev/null
+++ b/web/src/components/filter/FilterCheckBox.tsx
@@ -0,0 +1,32 @@
+import { LuCheck } from "react-icons/lu";
+import { Button } from "../ui/button";
+import { IconType } from "react-icons";
+
+type FilterCheckBoxProps = {
+ label: string;
+ CheckIcon?: IconType;
+ isChecked: boolean;
+ onCheckedChange: (isChecked: boolean) => void;
+};
+
+export default function FilterCheckBox({
+ label,
+ CheckIcon = LuCheck,
+ isChecked,
+ onCheckedChange,
+}: FilterCheckBoxProps) {
+ return (
+
+ );
+}
diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx
index 489a997fb..1d862998c 100644
--- a/web/src/components/filter/ReviewFilterGroup.tsx
+++ b/web/src/components/filter/ReviewFilterGroup.tsx
@@ -1,4 +1,3 @@
-import { LuCheck } from "react-icons/lu";
import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import useSWR from "swr";
@@ -16,11 +15,11 @@ import { ReviewFilter } from "@/types/review";
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import { FaCalendarAlt, FaFilter, FaVideo } from "react-icons/fa";
-import { IconType } from "react-icons";
import { isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
+import FilterCheckBox from "./FilterCheckBox";
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
@@ -479,32 +478,3 @@ function GeneralFilterButton({
);
}
-
-type FilterCheckBoxProps = {
- label: string;
- CheckIcon?: IconType;
- isChecked: boolean;
- onCheckedChange: (isChecked: boolean) => void;
-};
-
-function FilterCheckBox({
- label,
- CheckIcon = LuCheck,
- isChecked,
- onCheckedChange,
-}: FilterCheckBoxProps) {
- return (
-
- );
-}
diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts
index 7825e7f8f..a6c6b3864 100644
--- a/web/src/types/frigateConfig.ts
+++ b/web/src/types/frigateConfig.ts
@@ -204,9 +204,11 @@ export interface CameraConfig {
};
}
+export const GROUP_ICONS = ["car", "cat", "dog", "leaf"] as const;
+
export type CameraGroupConfig = {
cameras: string[];
- icon: string;
+ icon: (typeof GROUP_ICONS)[number];
order: number;
};