forked from Github/frigate
Redesign Recordings View (#10690)
* Use full width top bar * Make each item in review filter group optional * Remove export creation from export page * Consolidate packages and fix opening recording from event * Use common type for time range * Move timeline to separate component * Add events list view to recordings view * Fix loading of images * Fix incorrect labels * use overlay state for selected timeline type * Fix up for mobile view for now * replace overlay state * fix comparison * remove unused
This commit is contained in:
@@ -10,27 +10,9 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuRadioItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import axios from "axios";
|
||||
import { format } from "date-fns";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { useCallback, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
type ExportItem = {
|
||||
@@ -38,96 +20,13 @@ type ExportItem = {
|
||||
};
|
||||
|
||||
function Export() {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const { data: exports, mutate } = useSWR<ExportItem[]>(
|
||||
"exports/",
|
||||
(url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data),
|
||||
);
|
||||
const location = useLocation();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
// Export States
|
||||
const [camera, setCamera] = useState<string | undefined>();
|
||||
const [playback, setPlayback] = useState<string | undefined>();
|
||||
|
||||
const currentDate = new Date();
|
||||
currentDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const [date, setDate] = useState<DateRange | undefined>({
|
||||
from: currentDate,
|
||||
});
|
||||
const [startTime, setStartTime] = useState("00:00:00");
|
||||
const [endTime, setEndTime] = useState("23:59:59");
|
||||
|
||||
const [deleteClip, setDeleteClip] = useState<string | undefined>();
|
||||
|
||||
const onHandleExport = () => {
|
||||
if (!camera) {
|
||||
toast.error("A camera needs to be selected.", { position: "top-center" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playback) {
|
||||
toast.error("A playback factor needs to be selected.", {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!date?.from || !startTime || !endTime) {
|
||||
toast.error("A start and end time needs to be selected", {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date(date.from.getTime());
|
||||
const [startHour, startMin, startSec] = startTime.split(":");
|
||||
startDate.setHours(
|
||||
parseInt(startHour),
|
||||
parseInt(startMin),
|
||||
parseInt(startSec),
|
||||
0,
|
||||
);
|
||||
const start = startDate.getTime() / 1000;
|
||||
const endDate = new Date((date.to || date.from).getTime());
|
||||
const [endHour, endMin, endSec] = endTime.split(":");
|
||||
endDate.setHours(parseInt(endHour), parseInt(endMin), parseInt(endSec), 0);
|
||||
const end = endDate.getTime() / 1000;
|
||||
|
||||
if (end <= start) {
|
||||
toast.error("The end time must be after the start time.", {
|
||||
position: "top-center",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
axios
|
||||
.post(`export/${camera}/start/${start}/end/${end}`, { playback })
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
"Successfully started export. View the file in the /exports folder.",
|
||||
{ position: "top-center" },
|
||||
);
|
||||
}
|
||||
|
||||
mutate();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response?.data?.message) {
|
||||
toast.error(
|
||||
`Failed to start export: ${error.response.data.message}`,
|
||||
{ position: "top-center" },
|
||||
);
|
||||
} else {
|
||||
toast.error(`Failed to start export: ${error.message}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onHandleDelete = useCallback(() => {
|
||||
if (!deleteClip) {
|
||||
return;
|
||||
@@ -141,27 +40,6 @@ function Export() {
|
||||
});
|
||||
}, [deleteClip, mutate]);
|
||||
|
||||
const Create = isDesktop ? Dialog : Drawer;
|
||||
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
||||
|
||||
useEffect(() => {
|
||||
if (location.state && location.state.start && location.state.end) {
|
||||
const startTimeString = format(
|
||||
new Date(location.state.start * 1000),
|
||||
"HH:mm:ss",
|
||||
);
|
||||
const endTimeString = format(
|
||||
new Date(location.state.end * 1000),
|
||||
"HH:mm:ss",
|
||||
);
|
||||
setStartTime(startTimeString);
|
||||
setEndTime(endTimeString);
|
||||
|
||||
setDialogOpen(true);
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
return (
|
||||
<div className="size-full p-2 overflow-hidden flex flex-col">
|
||||
<Toaster />
|
||||
@@ -186,102 +64,6 @@ function Export() {
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<div className="w-full h-14">
|
||||
<Create open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<Trigger>
|
||||
<Button variant="select">New Export</Button>
|
||||
</Trigger>
|
||||
<Content className="flex flex-col justify-center items-center">
|
||||
<div className="w-full flex justify-evenly items-center mt-4 md:mt-0">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="capitalize" variant="secondary">
|
||||
{camera?.replaceAll("_", " ") || "Select Camera"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel className="flex justify-center items-center">
|
||||
Select Camera
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
value={camera}
|
||||
onValueChange={setCamera}
|
||||
>
|
||||
{Object.keys(config?.cameras || {}).map((item) => (
|
||||
<DropdownMenuRadioItem
|
||||
className="capitalize"
|
||||
key={item}
|
||||
value={item}
|
||||
>
|
||||
{item.replaceAll("_", " ")}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="capitalize" variant="secondary">
|
||||
{playback?.split("_")[0] || "Select Playback"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel className="flex justify-center items-center">
|
||||
Select Playback
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup
|
||||
value={playback}
|
||||
onValueChange={setPlayback}
|
||||
>
|
||||
<DropdownMenuRadioItem value="realtime">
|
||||
Realtime
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="timelapse_25x">
|
||||
Timelapse
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Calendar mode="range" selected={date} onSelect={setDate} />
|
||||
<div className="w-full flex justify-evenly">
|
||||
<input
|
||||
className="w-36 p-1 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
id="startTime"
|
||||
type="time"
|
||||
value={startTime}
|
||||
step="1"
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="w-36 p-1 mx-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
id="endTime"
|
||||
type="time"
|
||||
value={endTime}
|
||||
step="1"
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex items-center justify-between px-4">
|
||||
{`${
|
||||
date?.from ? format(date?.from, "LLL dd, y") : ""
|
||||
} ${startTime} -> ${
|
||||
date?.to ? format(date?.to, "LLL dd, y") : ""
|
||||
} ${endTime}`}
|
||||
<Button
|
||||
className="my-4"
|
||||
variant="select"
|
||||
onClick={() => onHandleExport()}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</Content>
|
||||
</Create>
|
||||
</div>
|
||||
|
||||
<div className="size-full overflow-hidden">
|
||||
{exports && (
|
||||
<div className="size-full grid gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 overflow-y-auto">
|
||||
|
||||
Reference in New Issue
Block a user