Add export dialog functionality

This commit is contained in:
Nicolas Mowen 2024-03-26 12:50:07 -06:00
parent c763d9e89d
commit 63df9d4338
4 changed files with 111 additions and 12 deletions

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useCallback, useState } from "react";
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@ -11,6 +11,10 @@ import {
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { ExportMode } from "@/types/filter";
import { FaArrowDown } from "react-icons/fa";
import axios from "axios";
import { toast } from "sonner";
const EXPORT_OPTIONS = [ const EXPORT_OPTIONS = [
"1", "1",
@ -23,13 +27,87 @@ const EXPORT_OPTIONS = [
] as const; ] as const;
type ExportOption = (typeof EXPORT_OPTIONS)[number]; type ExportOption = (typeof EXPORT_OPTIONS)[number];
export default function ExportDialog() { type ExportDialogProps = {
camera: string;
mode: ExportMode;
setMode: (mode: ExportMode) => void;
};
export default function ExportDialog({
camera,
mode,
setMode,
}: ExportDialogProps) {
const [selectedOption, setSelectedOption] = useState<ExportOption>("1"); const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
const onStartExport = useCallback(() => {
const now = new Date();
let end = now.getTime() / 1000;
let start;
switch (selectedOption) {
case "1":
now.setHours(now.getHours() - 1);
start = now.getTime() / 1000;
break;
case "4":
now.setHours(now.getHours() - 4);
start = now.getTime() / 1000;
break;
case "8":
now.setHours(now.getHours() - 8);
start = now.getTime() / 1000;
break;
case "12":
now.setHours(now.getHours() - 12);
start = now.getTime() / 1000;
break;
case "24":
now.setHours(now.getHours() - 24);
start = now.getTime() / 1000;
break;
case "custom":
end = 0;
break;
}
axios
.post(`export/${camera}/start/${start}/end/${end}`, {
playback: "realtime",
})
.then((response) => {
if (response.status == 200) {
toast.success(
"Successfully started export. View the file in the /exports folder.",
{ position: "top-center" },
);
}
})
.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",
});
}
});
}, [camera, selectedOption]);
return ( return (
<Dialog open> <Dialog open={mode == "select"}>
<DialogTrigger> <DialogTrigger asChild>
<div></div> <Button
className="flex items-center gap-2"
variant="secondary"
size="sm"
onClick={() => setMode("select")}
>
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
Export
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@ -61,8 +139,19 @@ export default function ExportDialog() {
})} })}
</RadioGroup> </RadioGroup>
<DialogFooter> <DialogFooter>
<DialogClose>Cancel</DialogClose> <DialogClose onClick={() => setMode("none")}>Cancel</DialogClose>
<Button variant="select" size="sm"> <Button
variant="select"
size="sm"
onClick={() => {
if (selectedOption == "timeline") {
setMode("timeline");
} else {
onStartExport();
setMode("none");
}
}}
>
{selectedOption == "timeline" ? "Select" : "Export"} {selectedOption == "timeline" ? "Select" : "Export"}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -10,7 +10,6 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Toaster } from "@/components/ui/sonner";
import axios from "axios"; import axios from "axios";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
@ -42,8 +41,6 @@ function Export() {
return ( return (
<div className="size-full p-2 overflow-hidden flex flex-col"> <div className="size-full p-2 overflow-hidden flex flex-col">
<Toaster />
<AlertDialog <AlertDialog
open={deleteClip != undefined} open={deleteClip != undefined}
onOpenChange={() => setDeleteClip(undefined)} onOpenChange={() => setDeleteClip(undefined)}

View File

@ -1,3 +1,5 @@
// allow any // allow any
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type FilterType = { [searchKey: string]: any }; export type FilterType = { [searchKey: string]: any };
export type ExportMode = "select" | "timeline" | "none";

View File

@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { useOverlayState } from "@/hooks/use-overlay-state"; import { useOverlayState } from "@/hooks/use-overlay-state";
import { ExportMode } from "@/types/filter";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { Preview } from "@/types/preview"; import { Preview } from "@/types/preview";
import { import {
@ -33,6 +34,7 @@ import { isDesktop, isMobile } from "react-device-detect";
import { FaCircle, FaVideo } from "react-icons/fa"; import { FaCircle, FaVideo } from "react-icons/fa";
import { IoMdArrowRoundBack } from "react-icons/io"; import { IoMdArrowRoundBack } from "react-icons/io";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Toaster } from "@/components/ui/sonner";
import useSWR from "swr"; import useSWR from "swr";
const SEGMENT_DURATION = 30; const SEGMENT_DURATION = 30;
@ -75,6 +77,10 @@ export function RecordingView({
[reviewItems, mainCamera], [reviewItems, mainCamera],
); );
// export
const [exportMode, setExportMode] = useState<ExportMode>("none");
// timeline // timeline
const [timelineType, setTimelineType] = useOverlayState<TimelineType>( const [timelineType, setTimelineType] = useOverlayState<TimelineType>(
@ -211,6 +217,7 @@ export function RecordingView({
return ( return (
<div ref={contentRef} className="size-full flex flex-col"> <div ref={contentRef} className="size-full flex flex-col">
<Toaster />
<div className={`w-full h-10 flex items-center justify-between pr-1`}> <div className={`w-full h-10 flex items-center justify-between pr-1`}>
<Button className="rounded-lg" onClick={() => navigate(-1)}> <Button className="rounded-lg" onClick={() => navigate(-1)}>
<IoMdArrowRoundBack className="size-5 mr-[10px]" /> <IoMdArrowRoundBack className="size-5 mr-[10px]" />
@ -246,7 +253,11 @@ export function RecordingView({
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
)} )}
<ExportDialog /> <ExportDialog
camera={mainCamera}
mode={exportMode}
setMode={setExportMode}
/>
<ReviewFilterGroup <ReviewFilterGroup
filters={["date", "general"]} filters={["date", "general"]}
reviewSummary={reviewSummary} reviewSummary={reviewSummary}
@ -285,7 +296,7 @@ export function RecordingView({
</div> </div>
<div <div
className={`flex h-full mb-2 justify-center overflow-hidden ${isDesktop ? "" : "flex-col"}`} className={`flex h-full my-2 justify-center overflow-hidden ${isDesktop ? "" : "flex-col"}`}
> >
<div className="flex flex-1 flex-wrap"> <div className="flex flex-1 flex-wrap">
<div <div