diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 76127c353..05d2d23c4 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -1,7 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Dialog, - DialogClose, DialogContent, DialogFooter, DialogHeader, @@ -25,6 +24,7 @@ import ReviewActivityCalendar from "./ReviewActivityCalendar"; import { SelectSeparator } from "../ui/select"; import { isDesktop } from "react-device-detect"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import SaveExportOverlay from "./SaveExportOverlay"; const EXPORT_OPTIONS = [ "1", @@ -55,74 +55,121 @@ export default function ExportDialog({ setRange, setMode, }: 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 Trigger = isDesktop ? DialogTrigger : DrawerTrigger; const Content = isDesktop ? DialogContent : DrawerContent; return ( - { - if (!open) { - setMode("none"); - } - }} - > - - - - + onStartExport()} + onCancel={() => setMode("none")} + /> + { + if (!open) { + setMode("none"); + } + }} > - setMode("none")} - /> - - + + + + + setMode("none")} + /> + + + ); } type ExportContentProps = { - camera: string; latestTime: number; currentTime: number; range?: TimeRange; - mode: ExportMode; + name: string; + onStartExport: () => void; + setName: (name: string) => void; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; onCancel: () => void; }; export function ExportContent({ - camera, latestTime, currentTime, range, - mode, + name, + onStartExport, + setName, setRange, setMode, onCancel, }: ExportContentProps) { const [selectedOption, setSelectedOption] = useState("1"); - const [name, setName] = useState(""); const onSelectTime = useCallback( (option: ExportOption) => { @@ -161,51 +208,6 @@ export function ExportContent({ [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 ( <> {isDesktop && ( @@ -273,6 +275,7 @@ export function ExportContent({ setMode("timeline"); } else { onStartExport(); + setSelectedOption("1"); setMode("none"); } }} diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 32d01decc..a3413ac8f 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Button } from "../ui/button"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; @@ -12,6 +12,9 @@ import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { GeneralFilterContent } from "../filter/ReviewFilterGroup"; import useSWR from "swr"; 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"]; type DrawerMode = "none" | "select" | "export" | "calendar" | "filter"; @@ -41,6 +44,47 @@ export default function MobileReviewSettingsDrawer({ const { data: config } = useSWR("config"); const [drawerMode, setDrawerMode] = useState("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(() => { if (!config) { return []; @@ -100,11 +144,12 @@ export default function MobileReviewSettingsDrawer({ } else if (drawerMode == "export") { content = ( { setMode(mode); @@ -198,28 +243,36 @@ export default function MobileReviewSettingsDrawer({ } return ( - { - if (!open) { - setDrawerMode("none"); - } - }} - > - - - - - {content} - - + <> + onStartExport()} + onCancel={() => setMode("none")} + /> + { + if (!open) { + setDrawerMode("none"); + } + }} + > + + + + + {content} + + + ); } diff --git a/web/src/components/overlay/SaveExportOverlay.tsx b/web/src/components/overlay/SaveExportOverlay.tsx new file mode 100644 index 000000000..79bef9c9f --- /dev/null +++ b/web/src/components/overlay/SaveExportOverlay.tsx @@ -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 ( +
+
+ + +
+
+ ); +} diff --git a/web/src/types/filter.ts b/web/src/types/filter.ts index 9cefd71df..228aea98f 100644 --- a/web/src/types/filter.ts +++ b/web/src/types/filter.ts @@ -2,4 +2,4 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type FilterType = { [searchKey: string]: any }; -export type ExportMode = "select" | "timeline" | "timeline_save" | "none"; +export type ExportMode = "select" | "timeline" | "none";