Add overlay instead of using same button for timeline exports

This commit is contained in:
Nicolas Mowen 2024-03-27 14:53:34 -06:00
parent c9934f7e4b
commit d59f23abdd
4 changed files with 208 additions and 115 deletions

View File

@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { import {
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
@ -25,6 +24,7 @@ import ReviewActivityCalendar from "./ReviewActivityCalendar";
import { SelectSeparator } from "../ui/select"; import { SelectSeparator } from "../ui/select";
import { isDesktop } from "react-device-detect"; import { isDesktop } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import SaveExportOverlay from "./SaveExportOverlay";
const EXPORT_OPTIONS = [ const EXPORT_OPTIONS = [
"1", "1",
@ -55,74 +55,121 @@ export default function ExportDialog({
setRange, setRange,
setMode, setMode,
}: ExportDialogProps) { }: ExportDialogProps) {
const [name, setName] = useState("");
const onStartExport = useCallback(() => {
if (!range) {
toast.error("No valid time range selected", { position: "top-center" });
return;
}
axios
.post(`export/${camera}/start/${range.after}/end/${range.before}`, {
playback: "realtime",
name,
})
.then((response) => {
if (response.status == 200) {
toast.success(
"Successfully started export. View the file in the /exports folder.",
{ position: "top-center" },
);
setName("");
setRange(undefined);
setMode("none");
}
})
.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, name, range, setRange, setName, setMode]);
const Overlay = isDesktop ? Dialog : Drawer; const Overlay = isDesktop ? Dialog : Drawer;
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger; const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
const Content = isDesktop ? DialogContent : DrawerContent; const Content = isDesktop ? DialogContent : DrawerContent;
return ( return (
<Overlay <>
open={mode == "select"} <SaveExportOverlay
onOpenChange={(open) => { className="absolute top-8 left-1/2 -translate-x-1/2 z-50 pointer-events-none"
if (!open) { show={mode == "timeline"}
setMode("none"); onSave={() => onStartExport()}
} onCancel={() => setMode("none")}
}} />
> <Overlay
<Trigger asChild> open={mode == "select"}
<Button onOpenChange={(open) => {
className="flex items-center gap-2" if (!open) {
variant="secondary" setMode("none");
size="sm" }
onClick={() => { }}
if (mode == "none") {
setMode("select");
}
}}
>
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
{isDesktop ? (mode != "timeline" ? "Export" : "Save") : null}
</Button>
</Trigger>
<Content
className={isDesktop ? "sm:rounded-2xl" : "px-4 pb-4 mx-4 rounded-2xl"}
> >
<ExportContent <Trigger asChild>
camera={camera} <Button
latestTime={latestTime} className="flex items-center gap-2"
currentTime={currentTime} variant="secondary"
range={range} size="sm"
mode={mode} onClick={() => {
setRange={setRange} setMode("select");
setMode={setMode} }}
onCancel={() => setMode("none")} >
/> <FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
</Content> {isDesktop && "Export"}
</Overlay> </Button>
</Trigger>
<Content
className={
isDesktop ? "sm:rounded-2xl" : "px-4 pb-4 mx-4 rounded-2xl"
}
>
<ExportContent
latestTime={latestTime}
currentTime={currentTime}
range={range}
name={name}
onStartExport={onStartExport}
setName={setName}
setRange={setRange}
setMode={setMode}
onCancel={() => setMode("none")}
/>
</Content>
</Overlay>
</>
); );
} }
type ExportContentProps = { type ExportContentProps = {
camera: string;
latestTime: number; latestTime: number;
currentTime: number; currentTime: number;
range?: TimeRange; range?: TimeRange;
mode: ExportMode; name: string;
onStartExport: () => void;
setName: (name: string) => void;
setRange: (range: TimeRange | undefined) => void; setRange: (range: TimeRange | undefined) => void;
setMode: (mode: ExportMode) => void; setMode: (mode: ExportMode) => void;
onCancel: () => void; onCancel: () => void;
}; };
export function ExportContent({ export function ExportContent({
camera,
latestTime, latestTime,
currentTime, currentTime,
range, range,
mode, name,
onStartExport,
setName,
setRange, setRange,
setMode, setMode,
onCancel, onCancel,
}: ExportContentProps) { }: ExportContentProps) {
const [selectedOption, setSelectedOption] = useState<ExportOption>("1"); const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
const [name, setName] = useState("");
const onSelectTime = useCallback( const onSelectTime = useCallback(
(option: ExportOption) => { (option: ExportOption) => {
@ -161,51 +208,6 @@ export function ExportContent({
[latestTime, setRange], [latestTime, setRange],
); );
const onStartExport = useCallback(() => {
if (!range) {
toast.error("No valid time range selected", { position: "top-center" });
return;
}
axios
.post(`export/${camera}/start/${range.after}/end/${range.before}`, {
playback: "realtime",
name,
})
.then((response) => {
if (response.status == 200) {
toast.success(
"Successfully started export. View the file in the /exports folder.",
{ position: "top-center" },
);
setName("");
setRange(undefined);
setSelectedOption("1");
}
})
.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, name, range, setRange]);
useEffect(() => {
if (mode != "timeline_save") {
return;
}
onStartExport();
setMode("none");
}, [mode, onStartExport, setMode]);
return ( return (
<> <>
{isDesktop && ( {isDesktop && (
@ -273,6 +275,7 @@ export function ExportContent({
setMode("timeline"); setMode("timeline");
} else { } else {
onStartExport(); onStartExport();
setSelectedOption("1");
setMode("none"); setMode("none");
} }
}} }}

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa";
@ -12,6 +12,9 @@ import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { GeneralFilterContent } from "../filter/ReviewFilterGroup"; import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { toast } from "sonner";
import axios from "axios";
import SaveExportOverlay from "./SaveExportOverlay";
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"]; const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter"; type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
@ -41,6 +44,47 @@ export default function MobileReviewSettingsDrawer({
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none"); const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
// exports
const [name, setName] = useState("");
const onStartExport = useCallback(() => {
if (!range) {
toast.error("No valid time range selected", { position: "top-center" });
return;
}
axios
.post(`export/${camera}/start/${range.after}/end/${range.before}`, {
playback: "realtime",
name,
})
.then((response) => {
if (response.status == 200) {
toast.success(
"Successfully started export. View the file in the /exports folder.",
{ position: "top-center" },
);
setName("");
setRange(undefined);
setMode("none");
}
})
.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, name, range, setRange, setName, setMode]);
// filters
const allLabels = useMemo<string[]>(() => { const allLabels = useMemo<string[]>(() => {
if (!config) { if (!config) {
return []; return [];
@ -100,11 +144,12 @@ export default function MobileReviewSettingsDrawer({
} else if (drawerMode == "export") { } else if (drawerMode == "export") {
content = ( content = (
<ExportContent <ExportContent
camera={camera}
latestTime={latestTime} latestTime={latestTime}
currentTime={currentTime} currentTime={currentTime}
range={range} range={range}
mode={mode} name={name}
onStartExport={onStartExport}
setName={setName}
setRange={setRange} setRange={setRange}
setMode={(mode) => { setMode={(mode) => {
setMode(mode); setMode(mode);
@ -198,28 +243,36 @@ export default function MobileReviewSettingsDrawer({
} }
return ( return (
<Drawer <>
open={drawerMode != "none"} <SaveExportOverlay
onOpenChange={(open) => { className="absolute top-8 left-1/2 -translate-x-1/2 z-50 pointer-events-none"
if (!open) { show={mode == "timeline"}
setDrawerMode("none"); onSave={() => onStartExport()}
} onCancel={() => setMode("none")}
}} />
> <Drawer
<DrawerTrigger asChild> open={drawerMode != "none"}
<Button onOpenChange={(open) => {
className="rounded-lg capitalize" if (!open) {
size="sm" setDrawerMode("none");
variant="secondary" }
onClick={() => setDrawerMode("select")} }}
> >
<FaCog className="text-muted-foreground" /> <DrawerTrigger asChild>
</Button> <Button
</DrawerTrigger> className="rounded-lg capitalize"
<DrawerContent className="max-h-[75dvh] overflow-hidden flex flex-col items-center gap-2 px-4 pb-4 mx-4 rounded-2xl"> size="sm"
{content} variant="secondary"
</DrawerContent> onClick={() => setDrawerMode("select")}
</Drawer> >
<FaCog className="text-muted-foreground" />
</Button>
</DrawerTrigger>
<DrawerContent className="max-h-[75dvh] overflow-hidden flex flex-col items-center gap-2 px-4 pb-4 mx-4 rounded-2xl">
{content}
</DrawerContent>
</Drawer>
</>
); );
} }

View File

@ -0,0 +1,37 @@
import { Button } from "../ui/button";
import { LuX } from "react-icons/lu";
type SaveExportOverlayProps = {
className: string;
show: boolean;
onSave: () => void;
onCancel: () => void;
};
export default function SaveExportOverlay({
className,
show,
onSave,
onCancel,
}: SaveExportOverlayProps) {
return (
<div className={className}>
<div
className={`flex justify-center px-2 gap-2 items-center pointer-events-auto bg-selected rounded-lg *:text-white *:hover:bg-transparent ${
show ? "animate-in slide-in-from-top duration-500" : "invisible"
} text-center mt-5 mx-auto`}
>
<Button className="p-0" variant="ghost" size="sm" onClick={onSave}>
Save Export
</Button>
<Button
className="cursor-pointer"
size="xs"
variant="ghost"
onClick={onCancel}
>
<LuX />
</Button>
</div>
</div>
);
}

View File

@ -2,4 +2,4 @@
// 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" | "timeline_save" | "none"; export type ExportMode = "select" | "timeline" | "none";