import { useCallback, useMemo, useState } from "react"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "../ui/dialog"; import { Label } from "../ui/label"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { Button } from "../ui/button"; import { ExportMode } from "@/types/filter"; import { FaArrowDown, FaArrowRight, FaCalendarAlt } from "react-icons/fa"; import axios from "axios"; import { toast } from "sonner"; import { Input } from "../ui/input"; import { TimeRange } from "@/types/timeline"; import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; 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", "4", "8", "12", "24", "timeline", "custom", ] as const; type ExportOption = (typeof EXPORT_OPTIONS)[number]; type ExportDialogProps = { camera: string; latestTime: number; currentTime: number; range?: TimeRange; mode: ExportMode; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; }; export default function ExportDialog({ camera, latestTime, currentTime, range, mode, 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 ( <> onStartExport()} onCancel={() => setMode("none")} /> { if (!open) { setMode("none"); } }} > setMode("none")} /> ); } type ExportContentProps = { latestTime: number; currentTime: number; range?: TimeRange; name: string; onStartExport: () => void; setName: (name: string) => void; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; onCancel: () => void; }; export function ExportContent({ latestTime, currentTime, range, name, onStartExport, setName, setRange, setMode, onCancel, }: ExportContentProps) { const [selectedOption, setSelectedOption] = useState("1"); const onSelectTime = useCallback( (option: ExportOption) => { setSelectedOption(option); const now = new Date(latestTime * 1000); let start = 0; switch (option) { 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; } setRange({ before: latestTime, after: start, }); }, [latestTime, setRange], ); return (
{isDesktop && ( <> Export )} onSelectTime(value as ExportOption)} > {EXPORT_OPTIONS.map((opt) => { return (
); })}
{selectedOption == "custom" && ( )} setName(e.target.value)} /> {isDesktop && }
Cancel
); } type CustomTimeSelectorProps = { latestTime: number; range?: TimeRange; setRange: (range: TimeRange | undefined) => void; }; function CustomTimeSelector({ latestTime, range, setRange, }: CustomTimeSelectorProps) { const { data: config } = useSWR("config"); // times const startTime = useMemo( () => range?.after || latestTime - 3600, [range, latestTime], ); const endTime = useMemo( () => range?.before || latestTime, [range, latestTime], ); const formattedStart = useFormattedTimestamp( startTime, config?.ui.time_format == "24hour" ? "%b %-d, %H:%M:%S" : "%b %-d, %I:%M:%S %p", ); const formattedEnd = useFormattedTimestamp( endTime, config?.ui.time_format == "24hour" ? "%b %-d, %H:%M:%S" : "%b %-d, %I:%M:%S %p", ); const startClock = useMemo(() => { const date = new Date(startTime * 1000); return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; }, [startTime]); const endClock = useMemo(() => { const date = new Date(endTime * 1000); return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; }, [endTime]); // calendars const [startOpen, setStartOpen] = useState(false); const [endOpen, setEndOpen] = useState(false); return (
{ if (!open) { setStartOpen(false); } }} > { if (!day) { return; } setRange({ before: endTime, after: day.getTime() / 1000 + 1, }); }} /> { const clock = e.target.value; const [hour, minute, second] = clock.split(":"); const start = new Date(startTime * 1000); start.setHours( parseInt(hour), parseInt(minute), parseInt(second), 0, ); setRange({ before: endTime, after: start.getTime() / 1000, }); }} /> { if (!open) { setEndOpen(false); } }} > { if (!day) { return; } setRange({ after: startTime, before: day.getTime() / 1000, }); }} /> { const clock = e.target.value; const [hour, minute, second] = clock.split(":"); const end = new Date(endTime * 1000); end.setHours( parseInt(hour), parseInt(minute), parseInt(second), 0, ); setRange({ before: end.getTime() / 1000, after: startTime, }); }} />
); }