mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 21:25:24 +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 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();
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user