Drag to reorder/resize cameras in camera groups (#11279)

* draggable/resizable cameras in camera groups on desktop/tablets

* fix edit button location on tablets

* assume 1rem is 16px
This commit is contained in:
Josh Hawkins
2024-05-07 09:28:10 -05:00
committed by GitHub
parent 08e5c791c8
commit ff2948a76b
9 changed files with 714 additions and 59 deletions

View File

@@ -59,6 +59,7 @@ import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
import ActivityIndicator from "../indicators/activity-indicator";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
import { usePersistence } from "@/hooks/use-persistence";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
@@ -89,7 +90,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
// groups
const [group, setGroup] = usePersistedOverlayState(
const [group, setGroup, deleteGroup] = usePersistedOverlayState(
"cameraGroup",
"default" as string,
);
@@ -118,6 +119,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
currentGroups={groups}
activeGroup={group}
setGroup={setGroup}
deleteGroup={deleteGroup}
/>
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
<div
@@ -198,6 +200,7 @@ type NewGroupDialogProps = {
currentGroups: [string, CameraGroupConfig][];
activeGroup?: string;
setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
deleteGroup: () => void;
};
function NewGroupDialog({
open,
@@ -205,6 +208,7 @@ function NewGroupDialog({
currentGroups,
activeGroup,
setGroup,
deleteGroup,
}: NewGroupDialogProps) {
const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
@@ -225,11 +229,16 @@ function NewGroupDialog({
const [editState, setEditState] = useState<"none" | "add" | "edit">("none");
const [isLoading, setIsLoading] = useState(false);
const [, , , deleteGridLayout] = usePersistence(
`${activeGroup}-draggable-layout`,
);
// callbacks
const onDeleteGroup = useCallback(
async (name: string) => {
// TODO: reset order on groups when deleting
deleteGridLayout();
deleteGroup();
await axios
.put(`config/set?camera_groups.${name}`, { requires_restart: 0 })
@@ -260,7 +269,14 @@ function NewGroupDialog({
setIsLoading(false);
});
},
[updateConfig, activeGroup, setGroup, setOpen],
[
updateConfig,
activeGroup,
setGroup,
setOpen,
deleteGroup,
deleteGridLayout,
],
);
const onSave = () => {
@@ -479,7 +495,11 @@ export function CameraGroupEdit({
{
message: "Camera group name already exists.",
},
),
)
.refine((value: string) => value.toLowerCase() !== "default", {
message: "Invalid camera group name.",
}),
cameras: z.array(z.string()).min(2, {
message: "You must select at least two cameras.",
}),

View File

@@ -1,19 +1,91 @@
import Heading from "@/components/ui/heading";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useEffect } from "react";
import { useCallback, useEffect } from "react";
import { Toaster } from "sonner";
import { toast } from "sonner";
import { Separator } from "../ui/separator";
import { Button } from "../ui/button";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { del as delData } from "idb-keyval";
export default function General() {
const { data: config } = useSWR<FrigateConfig>("config");
const clearStoredLayouts = useCallback(() => {
if (!config) {
return [];
}
Object.entries(config.camera_groups).forEach(async (value) => {
await delData(`${value[0]}-draggable-layout`)
.then(() => {
toast.success(`Cleared stored layout for ${value[0]}`, {
position: "top-center",
});
})
.catch((error) => {
toast.error(
`Failed to clear stored layout: ${error.response.data.message}`,
{ position: "top-center" },
);
});
});
}, [config]);
useEffect(() => {
document.title = "General Settings - Frigate";
}, []);
return (
<>
<Heading as="h2">Settings</Heading>
<div className="flex items-center space-x-2 mt-5">
<Switch id="lowdata" checked={false} onCheckedChange={() => {}} />
<Label htmlFor="lowdata">Low Data Mode (this device only)</Label>
<div className="flex flex-col md:flex-row size-full">
<Toaster position="top-center" closeButton={true} />
<div className="flex flex-col h-full w-full overflow-y-auto mt-2 md:mt-0 mb-10 md:mb-0 order-last md:order-none md:mr-2 rounded-lg border-secondary-foreground border-[1px] p-2 bg-background_alt">
<Heading as="h3" className="my-2">
General Settings
</Heading>
<div className="flex flex-col w-full space-y-6">
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<div className="text-md">Stored Layouts</div>
<div className="text-sm text-muted-foreground my-2">
<p>
The layout of cameras in a camera group can be
dragged/resized. The positions are stored in your browser's
local storage.
</p>
</div>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<Button onClick={clearStoredLayouts}>Clear All Layouts</Button>
</div>
</div>
<Separator className="flex my-2 bg-secondary" />
<div className="mt-2 space-y-6">
<div className="space-y-0.5">
<div className="text-md">Low Data Mode</div>
<div className="text-sm text-muted-foreground my-2">
<p>
Not yet implemented. <em>Default: disabled</em>
</p>
</div>
</div>
<div className="flex flex-row justify-start items-center gap-2">
<Switch
id="lowdata"
checked={false}
onCheckedChange={() => {}}
/>
<Label htmlFor="lowdata">
Low Data Mode (this device only)
</Label>
</div>
</div>
</div>
</div>
</div>
</>
);