diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 217e68fbb..1f97b953e 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -16,6 +16,7 @@ import { FaArrowDown } from "react-icons/fa"; import axios from "axios"; import { toast } from "sonner"; import { Input } from "../ui/input"; +import { TimeRange } from "@/types/timeline"; const EXPORT_OPTIONS = [ "1", @@ -30,50 +31,70 @@ type ExportOption = (typeof EXPORT_OPTIONS)[number]; type ExportDialogProps = { camera: string; + latestTime: number; + currentTime: number; + range?: TimeRange; mode: ExportMode; + setRange: (range: TimeRange) => void; setMode: (mode: ExportMode) => void; }; export default function ExportDialog({ camera, + latestTime, + currentTime, + range, mode, + setRange, setMode, }: ExportDialogProps) { const [selectedOption, setSelectedOption] = useState("1"); const [name, setName] = useState(""); - const onStartExport = useCallback(() => { - const now = new Date(); - let end = now.getTime() / 1000; + const onSelectTime = useCallback( + (option: ExportOption) => { + setSelectedOption(option); - 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; + 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], + ); + + const onStartExport = useCallback(() => { + if (!range) { + toast.error("No valid time range selected", { position: "top-center" }); + return; } axios - .post(`export/${camera}/start/${start}/end/${end}`, { + .post(`export/${camera}/start/${range.after}/end/${range.before}`, { playback: "realtime", name, }) @@ -97,10 +118,17 @@ export default function ExportDialog({ }); } }); - }, [camera, name, selectedOption]); + }, [camera, name, range]); return ( - + { + if (!open) { + setMode("none"); + } + }} + > @@ -117,7 +145,7 @@ export default function ExportDialog({ Export setSelectedOption(value as ExportOption)} + onValueChange={(value) => onSelectTime(value as ExportOption)} > {EXPORT_OPTIONS.map((opt) => { return ( @@ -154,6 +182,7 @@ export default function ExportDialog({ size="sm" onClick={() => { if (selectedOption == "timeline") { + setRange({ before: currentTime + 30, after: currentTime - 30 }); setMode("timeline"); } else { onStartExport(); diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 7b672a91b..ce6d03f1c 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -36,6 +36,7 @@ import { IoMdArrowRoundBack } from "react-icons/io"; import { useNavigate } from "react-router-dom"; import { Toaster } from "@/components/ui/sonner"; import useSWR from "swr"; +import { TimeRange } from "@/types/timeline"; const SEGMENT_DURATION = 30; type TimelineType = "timeline" | "events"; @@ -77,10 +78,6 @@ export function RecordingView({ [reviewItems, mainCamera], ); - // export - - const [exportMode, setExportMode] = useState("none"); - // timeline const [timelineType, setTimelineType] = useOverlayState( @@ -99,6 +96,11 @@ export function RecordingView({ [selectedRangeIdx, timeRange], ); + // export + + const [exportMode, setExportMode] = useState("none"); + const [exportRange, setExportRange] = useState(); + // move to next clip const onClipEnded = useCallback(() => { @@ -255,7 +257,11 @@ export function RecordingView({ )} { mainControllerRef.current = controller; }} - isScrubbing={scrubbing} + isScrubbing={scrubbing || exportMode == "timeline"} /> {isDesktop && ( @@ -395,8 +401,10 @@ export function RecordingView({ timeRange={timeRange} mainCameraReviewItems={mainCameraReviewItems} currentTime={currentTime} + exportRange={exportMode == "timeline" ? exportRange : undefined} setCurrentTime={setCurrentTime} setScrubbing={setScrubbing} + setExportRange={setExportRange} /> @@ -410,8 +418,10 @@ type TimelineProps = { timeRange: { start: number; end: number }; mainCameraReviewItems: ReviewSegment[]; currentTime: number; + exportRange?: TimeRange; setCurrentTime: React.Dispatch>; setScrubbing: React.Dispatch>; + setExportRange: (range: TimeRange) => void; }; function Timeline({ contentRef, @@ -420,8 +430,10 @@ function Timeline({ timeRange, mainCameraReviewItems, currentTime, + exportRange, setCurrentTime, setScrubbing, + setExportRange, }: TimelineProps) { const { data: motionData } = useSWR([ "review/activity/motion", @@ -433,7 +445,22 @@ function Timeline({ }, ]); - if (timelineType == "timeline") { + const [exportStart, setExportStartTime] = useState(0); + const [exportEnd, setExportEndTime] = useState(0); + + useEffect(() => { + if (exportRange && exportStart != 0 && exportEnd != 0) { + if (exportRange.after != exportStart) { + setCurrentTime(exportStart); + } else if (exportRange?.before != exportEnd) { + setCurrentTime(exportEnd); + } + + setExportRange({ after: exportStart, before: exportEnd }); + } + }, [exportRange, exportStart, exportEnd, setExportRange, setCurrentTime]); + + if (exportRange != undefined || timelineType == "timeline") { return (