mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 13:15:25 +03:00
Add overlay instead of using same button for timeline exports
This commit is contained in:
parent
c9934f7e4b
commit
d59f23abdd
@ -1,7 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
@ -25,6 +24,7 @@ import ReviewActivityCalendar from "./ReviewActivityCalendar";
|
|||||||
import { SelectSeparator } from "../ui/select";
|
import { SelectSeparator } from "../ui/select";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||||
|
import SaveExportOverlay from "./SaveExportOverlay";
|
||||||
|
|
||||||
const EXPORT_OPTIONS = [
|
const EXPORT_OPTIONS = [
|
||||||
"1",
|
"1",
|
||||||
@ -55,74 +55,121 @@ export default function ExportDialog({
|
|||||||
setRange,
|
setRange,
|
||||||
setMode,
|
setMode,
|
||||||
}: ExportDialogProps) {
|
}: 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 Overlay = isDesktop ? Dialog : Drawer;
|
||||||
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||||
const Content = isDesktop ? DialogContent : DrawerContent;
|
const Content = isDesktop ? DialogContent : DrawerContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<>
|
||||||
open={mode == "select"}
|
<SaveExportOverlay
|
||||||
onOpenChange={(open) => {
|
className="absolute top-8 left-1/2 -translate-x-1/2 z-50 pointer-events-none"
|
||||||
if (!open) {
|
show={mode == "timeline"}
|
||||||
setMode("none");
|
onSave={() => onStartExport()}
|
||||||
}
|
onCancel={() => setMode("none")}
|
||||||
}}
|
/>
|
||||||
>
|
<Overlay
|
||||||
<Trigger asChild>
|
open={mode == "select"}
|
||||||
<Button
|
onOpenChange={(open) => {
|
||||||
className="flex items-center gap-2"
|
if (!open) {
|
||||||
variant="secondary"
|
setMode("none");
|
||||||
size="sm"
|
}
|
||||||
onClick={() => {
|
}}
|
||||||
if (mode == "none") {
|
|
||||||
setMode("select");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
|
|
||||||
{isDesktop ? (mode != "timeline" ? "Export" : "Save") : null}
|
|
||||||
</Button>
|
|
||||||
</Trigger>
|
|
||||||
<Content
|
|
||||||
className={isDesktop ? "sm:rounded-2xl" : "px-4 pb-4 mx-4 rounded-2xl"}
|
|
||||||
>
|
>
|
||||||
<ExportContent
|
<Trigger asChild>
|
||||||
camera={camera}
|
<Button
|
||||||
latestTime={latestTime}
|
className="flex items-center gap-2"
|
||||||
currentTime={currentTime}
|
variant="secondary"
|
||||||
range={range}
|
size="sm"
|
||||||
mode={mode}
|
onClick={() => {
|
||||||
setRange={setRange}
|
setMode("select");
|
||||||
setMode={setMode}
|
}}
|
||||||
onCancel={() => setMode("none")}
|
>
|
||||||
/>
|
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
|
||||||
</Content>
|
{isDesktop && "Export"}
|
||||||
</Overlay>
|
</Button>
|
||||||
|
</Trigger>
|
||||||
|
<Content
|
||||||
|
className={
|
||||||
|
isDesktop ? "sm:rounded-2xl" : "px-4 pb-4 mx-4 rounded-2xl"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ExportContent
|
||||||
|
latestTime={latestTime}
|
||||||
|
currentTime={currentTime}
|
||||||
|
range={range}
|
||||||
|
name={name}
|
||||||
|
onStartExport={onStartExport}
|
||||||
|
setName={setName}
|
||||||
|
setRange={setRange}
|
||||||
|
setMode={setMode}
|
||||||
|
onCancel={() => setMode("none")}
|
||||||
|
/>
|
||||||
|
</Content>
|
||||||
|
</Overlay>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExportContentProps = {
|
type ExportContentProps = {
|
||||||
camera: string;
|
|
||||||
latestTime: number;
|
latestTime: number;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
range?: TimeRange;
|
range?: TimeRange;
|
||||||
mode: ExportMode;
|
name: string;
|
||||||
|
onStartExport: () => void;
|
||||||
|
setName: (name: string) => void;
|
||||||
setRange: (range: TimeRange | undefined) => void;
|
setRange: (range: TimeRange | undefined) => void;
|
||||||
setMode: (mode: ExportMode) => void;
|
setMode: (mode: ExportMode) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
export function ExportContent({
|
export function ExportContent({
|
||||||
camera,
|
|
||||||
latestTime,
|
latestTime,
|
||||||
currentTime,
|
currentTime,
|
||||||
range,
|
range,
|
||||||
mode,
|
name,
|
||||||
|
onStartExport,
|
||||||
|
setName,
|
||||||
setRange,
|
setRange,
|
||||||
setMode,
|
setMode,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: ExportContentProps) {
|
}: ExportContentProps) {
|
||||||
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
|
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
|
||||||
const [name, setName] = useState("");
|
|
||||||
|
|
||||||
const onSelectTime = useCallback(
|
const onSelectTime = useCallback(
|
||||||
(option: ExportOption) => {
|
(option: ExportOption) => {
|
||||||
@ -161,51 +208,6 @@ export function ExportContent({
|
|||||||
[latestTime, setRange],
|
[latestTime, setRange],
|
||||||
);
|
);
|
||||||
|
|
||||||
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);
|
|
||||||
setSelectedOption("1");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mode != "timeline_save") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onStartExport();
|
|
||||||
setMode("none");
|
|
||||||
}, [mode, onStartExport, setMode]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
@ -273,6 +275,7 @@ export function ExportContent({
|
|||||||
setMode("timeline");
|
setMode("timeline");
|
||||||
} else {
|
} else {
|
||||||
onStartExport();
|
onStartExport();
|
||||||
|
setSelectedOption("1");
|
||||||
setMode("none");
|
setMode("none");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa";
|
import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa";
|
||||||
@ -12,6 +12,9 @@ import { getEndOfDayTimestamp } from "@/utils/dateUtil";
|
|||||||
import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
|
import { GeneralFilterContent } from "../filter/ReviewFilterGroup";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import axios from "axios";
|
||||||
|
import SaveExportOverlay from "./SaveExportOverlay";
|
||||||
|
|
||||||
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
|
||||||
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
||||||
@ -41,6 +44,47 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
|
const [drawerMode, setDrawerMode] = useState<DrawerMode>("none");
|
||||||
|
|
||||||
|
// exports
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// filters
|
||||||
|
|
||||||
const allLabels = useMemo<string[]>(() => {
|
const allLabels = useMemo<string[]>(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return [];
|
return [];
|
||||||
@ -100,11 +144,12 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
} else if (drawerMode == "export") {
|
} else if (drawerMode == "export") {
|
||||||
content = (
|
content = (
|
||||||
<ExportContent
|
<ExportContent
|
||||||
camera={camera}
|
|
||||||
latestTime={latestTime}
|
latestTime={latestTime}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
range={range}
|
range={range}
|
||||||
mode={mode}
|
name={name}
|
||||||
|
onStartExport={onStartExport}
|
||||||
|
setName={setName}
|
||||||
setRange={setRange}
|
setRange={setRange}
|
||||||
setMode={(mode) => {
|
setMode={(mode) => {
|
||||||
setMode(mode);
|
setMode(mode);
|
||||||
@ -198,28 +243,36 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<>
|
||||||
open={drawerMode != "none"}
|
<SaveExportOverlay
|
||||||
onOpenChange={(open) => {
|
className="absolute top-8 left-1/2 -translate-x-1/2 z-50 pointer-events-none"
|
||||||
if (!open) {
|
show={mode == "timeline"}
|
||||||
setDrawerMode("none");
|
onSave={() => onStartExport()}
|
||||||
}
|
onCancel={() => setMode("none")}
|
||||||
}}
|
/>
|
||||||
>
|
<Drawer
|
||||||
<DrawerTrigger asChild>
|
open={drawerMode != "none"}
|
||||||
<Button
|
onOpenChange={(open) => {
|
||||||
className="rounded-lg capitalize"
|
if (!open) {
|
||||||
size="sm"
|
setDrawerMode("none");
|
||||||
variant="secondary"
|
}
|
||||||
onClick={() => setDrawerMode("select")}
|
}}
|
||||||
>
|
>
|
||||||
<FaCog className="text-muted-foreground" />
|
<DrawerTrigger asChild>
|
||||||
</Button>
|
<Button
|
||||||
</DrawerTrigger>
|
className="rounded-lg capitalize"
|
||||||
<DrawerContent className="max-h-[75dvh] overflow-hidden flex flex-col items-center gap-2 px-4 pb-4 mx-4 rounded-2xl">
|
size="sm"
|
||||||
{content}
|
variant="secondary"
|
||||||
</DrawerContent>
|
onClick={() => setDrawerMode("select")}
|
||||||
</Drawer>
|
>
|
||||||
|
<FaCog className="text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent className="max-h-[75dvh] overflow-hidden flex flex-col items-center gap-2 px-4 pb-4 mx-4 rounded-2xl">
|
||||||
|
{content}
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
37
web/src/components/overlay/SaveExportOverlay.tsx
Normal file
37
web/src/components/overlay/SaveExportOverlay.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { LuX } from "react-icons/lu";
|
||||||
|
|
||||||
|
type SaveExportOverlayProps = {
|
||||||
|
className: string;
|
||||||
|
show: boolean;
|
||||||
|
onSave: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
export default function SaveExportOverlay({
|
||||||
|
className,
|
||||||
|
show,
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
}: SaveExportOverlayProps) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div
|
||||||
|
className={`flex justify-center px-2 gap-2 items-center pointer-events-auto bg-selected rounded-lg *:text-white *:hover:bg-transparent ${
|
||||||
|
show ? "animate-in slide-in-from-top duration-500" : "invisible"
|
||||||
|
} text-center mt-5 mx-auto`}
|
||||||
|
>
|
||||||
|
<Button className="p-0" variant="ghost" size="sm" onClick={onSave}>
|
||||||
|
Save Export
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="cursor-pointer"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<LuX />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,4 +2,4 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type FilterType = { [searchKey: string]: any };
|
export type FilterType = { [searchKey: string]: any };
|
||||||
|
|
||||||
export type ExportMode = "select" | "timeline" | "timeline_save" | "none";
|
export type ExportMode = "select" | "timeline" | "none";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user