mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 13:15:25 +03:00
Add ability to choose custom time range on timeline
This commit is contained in:
parent
549b40d7c7
commit
3c1e266655
@ -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<ExportOption>("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 (
|
||||
<Dialog open={mode == "select"}>
|
||||
<Dialog
|
||||
open={mode == "select"}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setMode("none");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
@ -109,7 +137,7 @@ export default function ExportDialog({
|
||||
onClick={() => setMode("select")}
|
||||
>
|
||||
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
|
||||
Export
|
||||
{mode != "timeline" ? "Export" : "Save"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
@ -117,7 +145,7 @@ export default function ExportDialog({
|
||||
<DialogTitle>Export</DialogTitle>
|
||||
</DialogHeader>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => 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();
|
||||
|
||||
@ -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<ExportMode>("none");
|
||||
|
||||
// timeline
|
||||
|
||||
const [timelineType, setTimelineType] = useOverlayState<TimelineType>(
|
||||
@ -99,6 +96,11 @@ export function RecordingView({
|
||||
[selectedRangeIdx, timeRange],
|
||||
);
|
||||
|
||||
// export
|
||||
|
||||
const [exportMode, setExportMode] = useState<ExportMode>("none");
|
||||
const [exportRange, setExportRange] = useState<TimeRange>();
|
||||
|
||||
// move to next clip
|
||||
|
||||
const onClipEnded = useCallback(() => {
|
||||
@ -255,7 +257,11 @@ export function RecordingView({
|
||||
)}
|
||||
<ExportDialog
|
||||
camera={mainCamera}
|
||||
currentTime={currentTime}
|
||||
latestTime={timeRange.end}
|
||||
mode={exportMode}
|
||||
range={exportRange}
|
||||
setRange={setExportRange}
|
||||
setMode={setExportMode}
|
||||
/>
|
||||
<ReviewFilterGroup
|
||||
@ -327,7 +333,7 @@ export function RecordingView({
|
||||
onControllerReady={(controller) => {
|
||||
mainControllerRef.current = controller;
|
||||
}}
|
||||
isScrubbing={scrubbing}
|
||||
isScrubbing={scrubbing || exportMode == "timeline"}
|
||||
/>
|
||||
</div>
|
||||
{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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -410,8 +418,10 @@ type TimelineProps = {
|
||||
timeRange: { start: number; end: number };
|
||||
mainCameraReviewItems: ReviewSegment[];
|
||||
currentTime: number;
|
||||
exportRange?: TimeRange;
|
||||
setCurrentTime: React.Dispatch<React.SetStateAction<number>>;
|
||||
setScrubbing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
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<MotionData[]>([
|
||||
"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 (
|
||||
<div
|
||||
className={
|
||||
@ -447,7 +474,12 @@ function Timeline({
|
||||
timestampSpread={15}
|
||||
timelineStart={timeRange.end}
|
||||
timelineEnd={timeRange.start}
|
||||
showHandlebar
|
||||
showHandlebar={exportRange == undefined}
|
||||
showExportHandles={exportRange != undefined}
|
||||
exportStartTime={exportRange?.after}
|
||||
exportEndTime={exportRange?.before}
|
||||
setExportStartTime={setExportStartTime}
|
||||
setExportEndTime={setExportEndTime}
|
||||
handlebarTime={currentTime}
|
||||
setHandlebarTime={setCurrentTime}
|
||||
onlyInitialHandlebarScroll={true}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user