Add ability to choose custom time range on timeline

This commit is contained in:
Nicolas Mowen 2024-03-26 14:00:13 -06:00
parent 549b40d7c7
commit 3c1e266655
2 changed files with 101 additions and 40 deletions

View File

@ -16,6 +16,7 @@ import { FaArrowDown } from "react-icons/fa";
import axios from "axios"; import axios from "axios";
import { toast } from "sonner"; import { toast } from "sonner";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
import { TimeRange } from "@/types/timeline";
const EXPORT_OPTIONS = [ const EXPORT_OPTIONS = [
"1", "1",
@ -30,23 +31,32 @@ type ExportOption = (typeof EXPORT_OPTIONS)[number];
type ExportDialogProps = { type ExportDialogProps = {
camera: string; camera: string;
latestTime: number;
currentTime: number;
range?: TimeRange;
mode: ExportMode; mode: ExportMode;
setRange: (range: TimeRange) => void;
setMode: (mode: ExportMode) => void; setMode: (mode: ExportMode) => void;
}; };
export default function ExportDialog({ export default function ExportDialog({
camera, camera,
latestTime,
currentTime,
range,
mode, mode,
setRange,
setMode, setMode,
}: ExportDialogProps) { }: ExportDialogProps) {
const [selectedOption, setSelectedOption] = useState<ExportOption>("1"); const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
const [name, setName] = useState(""); const [name, setName] = useState("");
const onStartExport = useCallback(() => { const onSelectTime = useCallback(
const now = new Date(); (option: ExportOption) => {
let end = now.getTime() / 1000; setSelectedOption(option);
let start; const now = new Date(latestTime * 1000);
switch (selectedOption) { let start = 0;
switch (option) {
case "1": case "1":
now.setHours(now.getHours() - 1); now.setHours(now.getHours() - 1);
start = now.getTime() / 1000; start = now.getTime() / 1000;
@ -67,13 +77,24 @@ export default function ExportDialog({
now.setHours(now.getHours() - 24); now.setHours(now.getHours() - 24);
start = now.getTime() / 1000; start = now.getTime() / 1000;
break; break;
case "custom": }
end = 0;
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 axios
.post(`export/${camera}/start/${start}/end/${end}`, { .post(`export/${camera}/start/${range.after}/end/${range.before}`, {
playback: "realtime", playback: "realtime",
name, name,
}) })
@ -97,10 +118,17 @@ export default function ExportDialog({
}); });
} }
}); });
}, [camera, name, selectedOption]); }, [camera, name, range]);
return ( return (
<Dialog open={mode == "select"}> <Dialog
open={mode == "select"}
onOpenChange={(open) => {
if (!open) {
setMode("none");
}
}}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
className="flex items-center gap-2" className="flex items-center gap-2"
@ -109,7 +137,7 @@ export default function ExportDialog({
onClick={() => setMode("select")} onClick={() => setMode("select")}
> >
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" /> <FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
Export {mode != "timeline" ? "Export" : "Save"}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
@ -117,7 +145,7 @@ export default function ExportDialog({
<DialogTitle>Export</DialogTitle> <DialogTitle>Export</DialogTitle>
</DialogHeader> </DialogHeader>
<RadioGroup <RadioGroup
onValueChange={(value) => setSelectedOption(value as ExportOption)} onValueChange={(value) => onSelectTime(value as ExportOption)}
> >
{EXPORT_OPTIONS.map((opt) => { {EXPORT_OPTIONS.map((opt) => {
return ( return (
@ -154,6 +182,7 @@ export default function ExportDialog({
size="sm" size="sm"
onClick={() => { onClick={() => {
if (selectedOption == "timeline") { if (selectedOption == "timeline") {
setRange({ before: currentTime + 30, after: currentTime - 30 });
setMode("timeline"); setMode("timeline");
} else { } else {
onStartExport(); onStartExport();

View File

@ -36,6 +36,7 @@ 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 { Toaster } from "@/components/ui/sonner";
import useSWR from "swr"; import useSWR from "swr";
import { TimeRange } from "@/types/timeline";
const SEGMENT_DURATION = 30; const SEGMENT_DURATION = 30;
type TimelineType = "timeline" | "events"; type TimelineType = "timeline" | "events";
@ -77,10 +78,6 @@ 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>(
@ -99,6 +96,11 @@ export function RecordingView({
[selectedRangeIdx, timeRange], [selectedRangeIdx, timeRange],
); );
// export
const [exportMode, setExportMode] = useState<ExportMode>("none");
const [exportRange, setExportRange] = useState<TimeRange>();
// move to next clip // move to next clip
const onClipEnded = useCallback(() => { const onClipEnded = useCallback(() => {
@ -255,7 +257,11 @@ export function RecordingView({
)} )}
<ExportDialog <ExportDialog
camera={mainCamera} camera={mainCamera}
currentTime={currentTime}
latestTime={timeRange.end}
mode={exportMode} mode={exportMode}
range={exportRange}
setRange={setExportRange}
setMode={setExportMode} setMode={setExportMode}
/> />
<ReviewFilterGroup <ReviewFilterGroup
@ -327,7 +333,7 @@ export function RecordingView({
onControllerReady={(controller) => { onControllerReady={(controller) => {
mainControllerRef.current = controller; mainControllerRef.current = controller;
}} }}
isScrubbing={scrubbing} isScrubbing={scrubbing || exportMode == "timeline"}
/> />
</div> </div>
{isDesktop && ( {isDesktop && (
@ -395,8 +401,10 @@ export function RecordingView({
timeRange={timeRange} timeRange={timeRange}
mainCameraReviewItems={mainCameraReviewItems} mainCameraReviewItems={mainCameraReviewItems}
currentTime={currentTime} currentTime={currentTime}
exportRange={exportMode == "timeline" ? exportRange : undefined}
setCurrentTime={setCurrentTime} setCurrentTime={setCurrentTime}
setScrubbing={setScrubbing} setScrubbing={setScrubbing}
setExportRange={setExportRange}
/> />
</div> </div>
</div> </div>
@ -410,8 +418,10 @@ type TimelineProps = {
timeRange: { start: number; end: number }; timeRange: { start: number; end: number };
mainCameraReviewItems: ReviewSegment[]; mainCameraReviewItems: ReviewSegment[];
currentTime: number; currentTime: number;
exportRange?: TimeRange;
setCurrentTime: React.Dispatch<React.SetStateAction<number>>; setCurrentTime: React.Dispatch<React.SetStateAction<number>>;
setScrubbing: React.Dispatch<React.SetStateAction<boolean>>; setScrubbing: React.Dispatch<React.SetStateAction<boolean>>;
setExportRange: (range: TimeRange) => void;
}; };
function Timeline({ function Timeline({
contentRef, contentRef,
@ -420,8 +430,10 @@ function Timeline({
timeRange, timeRange,
mainCameraReviewItems, mainCameraReviewItems,
currentTime, currentTime,
exportRange,
setCurrentTime, setCurrentTime,
setScrubbing, setScrubbing,
setExportRange,
}: TimelineProps) { }: TimelineProps) {
const { data: motionData } = useSWR<MotionData[]>([ const { data: motionData } = useSWR<MotionData[]>([
"review/activity/motion", "review/activity/motion",
@ -433,7 +445,22 @@ function Timeline({
}, },
]); ]);
if (timelineType == "timeline") { const [exportStart, setExportStartTime] = useState<number>(0);
const [exportEnd, setExportEndTime] = useState<number>(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 ( return (
<div <div
className={ className={
@ -447,7 +474,12 @@ function Timeline({
timestampSpread={15} timestampSpread={15}
timelineStart={timeRange.end} timelineStart={timeRange.end}
timelineEnd={timeRange.start} timelineEnd={timeRange.start}
showHandlebar showHandlebar={exportRange == undefined}
showExportHandles={exportRange != undefined}
exportStartTime={exportRange?.after}
exportEndTime={exportRange?.before}
setExportStartTime={setExportStartTime}
setExportEndTime={setExportEndTime}
handlebarTime={currentTime} handlebarTime={currentTime}
setHandlebarTime={setCurrentTime} setHandlebarTime={setCurrentTime}
onlyInitialHandlebarScroll={true} onlyInitialHandlebarScroll={true}