2026-03-01 00:57:32 +03:00
|
|
|
import { useCallback, useState } from "react";
|
2024-03-27 00:37:45 +03:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
2024-10-13 20:46:40 +03:00
|
|
|
DialogDescription,
|
2024-03-27 00:37:45 +03:00
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogTrigger,
|
|
|
|
|
} from "../ui/dialog";
|
|
|
|
|
import { Label } from "../ui/label";
|
|
|
|
|
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
|
|
|
|
|
import { Button } from "../ui/button";
|
|
|
|
|
import { ExportMode } from "@/types/filter";
|
2026-03-01 00:57:32 +03:00
|
|
|
import { FaArrowDown } from "react-icons/fa";
|
2024-03-27 00:37:45 +03:00
|
|
|
import axios from "axios";
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import { Input } from "../ui/input";
|
|
|
|
|
import { TimeRange } from "@/types/timeline";
|
|
|
|
|
import useSWR from "swr";
|
2026-01-03 18:03:33 +03:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectSeparator,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "../ui/select";
|
2026-03-01 00:57:32 +03:00
|
|
|
import { isDesktop, isMobile } from "react-device-detect";
|
2024-03-28 02:03:05 +03:00
|
|
|
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
|
|
|
|
import SaveExportOverlay from "./SaveExportOverlay";
|
2024-10-13 20:46:40 +03:00
|
|
|
import { baseUrl } from "@/api/baseUrl";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
|
2025-03-16 18:36:20 +03:00
|
|
|
import { useTranslation } from "react-i18next";
|
2026-01-03 18:03:33 +03:00
|
|
|
import { ExportCase } from "@/types/export";
|
2026-03-01 00:57:32 +03:00
|
|
|
import { CustomTimeSelector } from "./CustomTimeSelector";
|
2024-03-27 00:37:45 +03:00
|
|
|
|
|
|
|
|
const EXPORT_OPTIONS = [
|
|
|
|
|
"1",
|
|
|
|
|
"4",
|
|
|
|
|
"8",
|
|
|
|
|
"12",
|
|
|
|
|
"24",
|
|
|
|
|
"timeline",
|
|
|
|
|
"custom",
|
|
|
|
|
] as const;
|
|
|
|
|
type ExportOption = (typeof EXPORT_OPTIONS)[number];
|
|
|
|
|
|
|
|
|
|
type ExportDialogProps = {
|
|
|
|
|
camera: string;
|
|
|
|
|
latestTime: number;
|
|
|
|
|
currentTime: number;
|
|
|
|
|
range?: TimeRange;
|
|
|
|
|
mode: ExportMode;
|
2024-10-13 20:46:40 +03:00
|
|
|
showPreview: boolean;
|
2024-03-27 00:37:45 +03:00
|
|
|
setRange: (range: TimeRange | undefined) => void;
|
|
|
|
|
setMode: (mode: ExportMode) => void;
|
2024-10-13 20:46:40 +03:00
|
|
|
setShowPreview: (showPreview: boolean) => void;
|
2024-03-27 00:37:45 +03:00
|
|
|
};
|
|
|
|
|
export default function ExportDialog({
|
|
|
|
|
camera,
|
|
|
|
|
latestTime,
|
|
|
|
|
currentTime,
|
|
|
|
|
range,
|
|
|
|
|
mode,
|
2024-10-13 20:46:40 +03:00
|
|
|
showPreview,
|
2024-03-27 00:37:45 +03:00
|
|
|
setRange,
|
|
|
|
|
setMode,
|
2024-10-13 20:46:40 +03:00
|
|
|
setShowPreview,
|
2024-03-27 00:37:45 +03:00
|
|
|
}: ExportDialogProps) {
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation(["components/dialog"]);
|
2024-03-27 00:37:45 +03:00
|
|
|
const [name, setName] = useState("");
|
2026-01-03 18:03:33 +03:00
|
|
|
const [selectedCaseId, setSelectedCaseId] = useState<string | undefined>(
|
|
|
|
|
undefined,
|
|
|
|
|
);
|
2024-10-13 20:46:40 +03:00
|
|
|
|
2024-03-28 02:03:05 +03:00
|
|
|
const onStartExport = useCallback(() => {
|
|
|
|
|
if (!range) {
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(t("export.toast.error.noVaildTimeSelected"), {
|
|
|
|
|
position: "top-center",
|
|
|
|
|
});
|
2024-03-28 02:03:05 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-16 18:04:33 +03:00
|
|
|
if (range.before < range.after) {
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(t("export.toast.error.endTimeMustAfterStartTime"), {
|
2024-07-16 18:04:33 +03:00
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-28 02:03:05 +03:00
|
|
|
axios
|
2024-04-07 23:35:45 +03:00
|
|
|
.post(
|
|
|
|
|
`export/${camera}/start/${Math.round(range.after)}/end/${Math.round(range.before)}`,
|
|
|
|
|
{
|
|
|
|
|
playback: "realtime",
|
|
|
|
|
name,
|
2026-01-03 18:03:33 +03:00
|
|
|
export_case_id: selectedCaseId || undefined,
|
2024-04-07 23:35:45 +03:00
|
|
|
},
|
|
|
|
|
)
|
2024-03-28 02:03:05 +03:00
|
|
|
.then((response) => {
|
|
|
|
|
if (response.status == 200) {
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.success(t("export.toast.success"), {
|
|
|
|
|
position: "top-center",
|
2025-10-26 21:11:48 +03:00
|
|
|
action: (
|
|
|
|
|
<a href="/export" target="_blank" rel="noopener noreferrer">
|
2025-11-25 16:34:20 +03:00
|
|
|
<Button>{t("export.toast.view")}</Button>
|
2025-10-26 21:11:48 +03:00
|
|
|
</a>
|
|
|
|
|
),
|
2025-03-16 18:36:20 +03:00
|
|
|
});
|
2024-03-28 02:03:05 +03:00
|
|
|
setName("");
|
2026-01-03 18:03:33 +03:00
|
|
|
setSelectedCaseId(undefined);
|
2024-03-28 02:03:05 +03:00
|
|
|
setRange(undefined);
|
|
|
|
|
setMode("none");
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
2025-03-08 19:01:08 +03:00
|
|
|
const errorMessage =
|
|
|
|
|
error.response?.data?.message ||
|
|
|
|
|
error.response?.data?.detail ||
|
|
|
|
|
"Unknown error";
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(
|
|
|
|
|
t("export.toast.error.failed", {
|
|
|
|
|
error: errorMessage,
|
|
|
|
|
}),
|
|
|
|
|
{ position: "top-center" },
|
|
|
|
|
);
|
2024-03-28 02:03:05 +03:00
|
|
|
});
|
2026-01-03 18:03:33 +03:00
|
|
|
}, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]);
|
2024-03-28 02:03:05 +03:00
|
|
|
|
2025-02-18 21:50:32 +03:00
|
|
|
const handleCancel = useCallback(() => {
|
|
|
|
|
setName("");
|
2026-01-03 18:03:33 +03:00
|
|
|
setSelectedCaseId(undefined);
|
2025-02-18 21:50:32 +03:00
|
|
|
setMode("none");
|
|
|
|
|
setRange(undefined);
|
|
|
|
|
}, [setMode, setRange]);
|
|
|
|
|
|
2024-03-28 02:03:05 +03:00
|
|
|
const Overlay = isDesktop ? Dialog : Drawer;
|
|
|
|
|
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
|
|
|
|
const Content = isDesktop ? DialogContent : DrawerContent;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2024-10-13 20:46:40 +03:00
|
|
|
<ExportPreviewDialog
|
|
|
|
|
camera={camera}
|
|
|
|
|
range={range}
|
|
|
|
|
showPreview={showPreview}
|
|
|
|
|
setShowPreview={setShowPreview}
|
|
|
|
|
/>
|
2024-03-28 02:03:05 +03:00
|
|
|
<SaveExportOverlay
|
2024-05-14 18:06:44 +03:00
|
|
|
className="pointer-events-none absolute left-1/2 top-8 z-50 -translate-x-1/2"
|
2024-03-28 02:03:05 +03:00
|
|
|
show={mode == "timeline"}
|
2024-10-13 20:46:40 +03:00
|
|
|
onPreview={() => setShowPreview(true)}
|
2024-03-28 02:03:05 +03:00
|
|
|
onSave={() => onStartExport()}
|
2025-02-18 21:50:32 +03:00
|
|
|
onCancel={handleCancel}
|
2024-03-28 02:03:05 +03:00
|
|
|
/>
|
|
|
|
|
<Overlay
|
|
|
|
|
open={mode == "select"}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setMode("none");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-03-01 00:57:32 +03:00
|
|
|
{!isDesktop && (
|
|
|
|
|
<Trigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
aria-label={t("menu.export", { ns: "common" })}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const now = new Date(latestTime * 1000);
|
|
|
|
|
let start = 0;
|
|
|
|
|
now.setHours(now.getHours() - 1);
|
|
|
|
|
start = now.getTime() / 1000;
|
|
|
|
|
setRange({
|
|
|
|
|
before: latestTime,
|
|
|
|
|
after: start,
|
|
|
|
|
});
|
|
|
|
|
setMode("select");
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
|
|
|
|
|
{isDesktop && (
|
|
|
|
|
<div className="text-primary">
|
|
|
|
|
{t("menu.export", { ns: "common" })}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</Trigger>
|
|
|
|
|
)}
|
2024-03-28 02:03:05 +03:00
|
|
|
<Content
|
|
|
|
|
className={
|
2024-04-22 18:12:45 +03:00
|
|
|
isDesktop
|
|
|
|
|
? "sm:rounded-lg md:rounded-2xl"
|
2024-05-14 18:06:44 +03:00
|
|
|
: "mx-4 rounded-lg px-4 pb-4 md:rounded-2xl"
|
2024-03-28 02:03:05 +03:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<ExportContent
|
|
|
|
|
latestTime={latestTime}
|
|
|
|
|
currentTime={currentTime}
|
|
|
|
|
range={range}
|
|
|
|
|
name={name}
|
2026-01-03 18:03:33 +03:00
|
|
|
selectedCaseId={selectedCaseId}
|
2024-03-28 02:03:05 +03:00
|
|
|
onStartExport={onStartExport}
|
|
|
|
|
setName={setName}
|
2026-01-03 18:03:33 +03:00
|
|
|
setSelectedCaseId={setSelectedCaseId}
|
2024-03-28 02:03:05 +03:00
|
|
|
setRange={setRange}
|
|
|
|
|
setMode={setMode}
|
2025-02-18 21:50:32 +03:00
|
|
|
onCancel={handleCancel}
|
2024-03-28 02:03:05 +03:00
|
|
|
/>
|
|
|
|
|
</Content>
|
|
|
|
|
</Overlay>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ExportContentProps = {
|
|
|
|
|
latestTime: number;
|
|
|
|
|
currentTime: number;
|
|
|
|
|
range?: TimeRange;
|
|
|
|
|
name: string;
|
2026-01-03 18:03:33 +03:00
|
|
|
selectedCaseId?: string;
|
2024-03-28 02:03:05 +03:00
|
|
|
onStartExport: () => void;
|
|
|
|
|
setName: (name: string) => void;
|
2026-01-03 18:03:33 +03:00
|
|
|
setSelectedCaseId: (caseId: string | undefined) => void;
|
2024-03-28 02:03:05 +03:00
|
|
|
setRange: (range: TimeRange | undefined) => void;
|
|
|
|
|
setMode: (mode: ExportMode) => void;
|
|
|
|
|
onCancel: () => void;
|
|
|
|
|
};
|
|
|
|
|
export function ExportContent({
|
|
|
|
|
latestTime,
|
|
|
|
|
currentTime,
|
|
|
|
|
range,
|
|
|
|
|
name,
|
2026-01-03 18:03:33 +03:00
|
|
|
selectedCaseId,
|
2024-03-28 02:03:05 +03:00
|
|
|
onStartExport,
|
|
|
|
|
setName,
|
2026-01-03 18:03:33 +03:00
|
|
|
setSelectedCaseId,
|
2024-03-28 02:03:05 +03:00
|
|
|
setRange,
|
|
|
|
|
setMode,
|
|
|
|
|
onCancel,
|
|
|
|
|
}: ExportContentProps) {
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation(["components/dialog"]);
|
2024-03-28 02:03:05 +03:00
|
|
|
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
|
2026-01-03 18:03:33 +03:00
|
|
|
const { data: cases } = useSWR<ExportCase[]>("cases");
|
2024-03-27 00:37:45 +03:00
|
|
|
|
|
|
|
|
const onSelectTime = useCallback(
|
|
|
|
|
(option: ExportOption) => {
|
|
|
|
|
setSelectedOption(option);
|
|
|
|
|
|
|
|
|
|
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;
|
2025-02-09 16:36:55 +03:00
|
|
|
case "custom":
|
|
|
|
|
start = latestTime - 3600;
|
|
|
|
|
break;
|
2024-03-27 00:37:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setRange({
|
|
|
|
|
before: latestTime,
|
|
|
|
|
after: start,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[latestTime, setRange],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2024-03-28 02:03:05 +03:00
|
|
|
<div className="w-full">
|
|
|
|
|
{isDesktop && (
|
|
|
|
|
<>
|
|
|
|
|
<DialogHeader>
|
2025-03-17 15:26:01 +03:00
|
|
|
<DialogTitle>{t("menu.export", { ns: "common" })}</DialogTitle>
|
2024-03-28 02:03:05 +03:00
|
|
|
</DialogHeader>
|
2024-04-04 18:43:54 +03:00
|
|
|
<SelectSeparator className="my-4 bg-secondary" />
|
2024-03-28 02:03:05 +03:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<RadioGroup
|
2024-04-04 18:43:54 +03:00
|
|
|
className={`flex flex-col gap-4 ${isDesktop ? "" : "mt-4"}`}
|
2024-03-28 02:03:05 +03:00
|
|
|
onValueChange={(value) => onSelectTime(value as ExportOption)}
|
|
|
|
|
>
|
|
|
|
|
{EXPORT_OPTIONS.map((opt) => {
|
|
|
|
|
return (
|
|
|
|
|
<div key={opt} className="flex items-center gap-2">
|
|
|
|
|
<RadioGroupItem
|
|
|
|
|
className={
|
|
|
|
|
opt == selectedOption
|
2024-05-14 18:06:44 +03:00
|
|
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
|
|
|
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
2024-03-28 02:03:05 +03:00
|
|
|
}
|
|
|
|
|
id={opt}
|
|
|
|
|
value={opt}
|
|
|
|
|
/>
|
2025-04-23 01:21:09 +03:00
|
|
|
<Label className="cursor-pointer smart-capitalize" htmlFor={opt}>
|
2024-03-28 02:03:05 +03:00
|
|
|
{isNaN(parseInt(opt))
|
|
|
|
|
? opt == "timeline"
|
2025-03-16 18:36:20 +03:00
|
|
|
? t("export.time.fromTimeline")
|
|
|
|
|
: t("export.time." + opt)
|
|
|
|
|
: t("export.time.lastHour", {
|
|
|
|
|
count: parseInt(opt),
|
|
|
|
|
})}
|
2024-03-28 02:03:05 +03:00
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</RadioGroup>
|
|
|
|
|
{selectedOption == "custom" && (
|
|
|
|
|
<CustomTimeSelector
|
|
|
|
|
latestTime={latestTime}
|
|
|
|
|
range={range}
|
|
|
|
|
setRange={setRange}
|
2026-03-01 00:57:32 +03:00
|
|
|
startLabel={t("export.time.start.title")}
|
|
|
|
|
endLabel={t("export.time.end.title")}
|
2024-03-28 02:03:05 +03:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<Input
|
2024-09-13 06:07:35 +03:00
|
|
|
className="text-md my-6"
|
2024-03-28 02:03:05 +03:00
|
|
|
type="search"
|
2025-03-16 18:36:20 +03:00
|
|
|
placeholder={t("export.name.placeholder")}
|
2024-03-28 02:03:05 +03:00
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
/>
|
2026-01-03 18:03:33 +03:00
|
|
|
<div className="my-4">
|
|
|
|
|
<Label className="text-sm text-secondary-foreground">
|
|
|
|
|
{t("export.case.label", { defaultValue: "Case (optional)" })}
|
|
|
|
|
</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={selectedCaseId || "none"}
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
setSelectedCaseId(value === "none" ? undefined : value)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="mt-2">
|
|
|
|
|
<SelectValue
|
|
|
|
|
placeholder={t("export.case.placeholder", {
|
|
|
|
|
defaultValue: "Select a case (optional)",
|
|
|
|
|
})}
|
|
|
|
|
/>
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem
|
|
|
|
|
value="none"
|
|
|
|
|
className="cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
|
|
|
|
>
|
|
|
|
|
{t("label.none", { ns: "common" })}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
{cases
|
|
|
|
|
?.sort((a, b) => a.name.localeCompare(b.name))
|
|
|
|
|
.map((caseItem) => (
|
|
|
|
|
<SelectItem
|
|
|
|
|
key={caseItem.id}
|
|
|
|
|
value={caseItem.id}
|
|
|
|
|
className="cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
|
|
|
|
>
|
|
|
|
|
{caseItem.name}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
2024-04-04 18:43:54 +03:00
|
|
|
{isDesktop && <SelectSeparator className="my-4 bg-secondary" />}
|
2024-03-28 02:03:05 +03:00
|
|
|
<DialogFooter
|
|
|
|
|
className={isDesktop ? "" : "mt-3 flex flex-col-reverse gap-4"}
|
|
|
|
|
>
|
|
|
|
|
<div
|
2024-05-14 18:06:44 +03:00
|
|
|
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
|
2024-03-28 02:03:05 +03:00
|
|
|
onClick={onCancel}
|
|
|
|
|
>
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("button.cancel", { ns: "common" })}
|
2024-03-28 02:03:05 +03:00
|
|
|
</div>
|
2024-03-27 00:37:45 +03:00
|
|
|
<Button
|
2024-03-28 02:03:05 +03:00
|
|
|
className={isDesktop ? "" : "w-full"}
|
2025-03-16 18:36:20 +03:00
|
|
|
aria-label={t("export.selectOrExport")}
|
2024-03-28 02:03:05 +03:00
|
|
|
variant="select"
|
2024-03-27 00:37:45 +03:00
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
2024-03-28 02:03:05 +03:00
|
|
|
if (selectedOption == "timeline") {
|
|
|
|
|
setRange({ before: currentTime + 30, after: currentTime - 30 });
|
|
|
|
|
setMode("timeline");
|
|
|
|
|
} else {
|
2024-03-27 00:37:45 +03:00
|
|
|
onStartExport();
|
2024-03-28 02:03:05 +03:00
|
|
|
setSelectedOption("1");
|
2024-03-27 00:37:45 +03:00
|
|
|
setMode("none");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-03-16 18:36:20 +03:00
|
|
|
{selectedOption == "timeline"
|
|
|
|
|
? t("export.select")
|
|
|
|
|
: t("export.export")}
|
2024-03-27 00:37:45 +03:00
|
|
|
</Button>
|
2024-03-28 02:03:05 +03:00
|
|
|
</DialogFooter>
|
|
|
|
|
</div>
|
2024-03-27 00:37:45 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-13 20:46:40 +03:00
|
|
|
type ExportPreviewDialogProps = {
|
|
|
|
|
camera: string;
|
|
|
|
|
range?: TimeRange;
|
|
|
|
|
showPreview: boolean;
|
|
|
|
|
setShowPreview: (showPreview: boolean) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function ExportPreviewDialog({
|
|
|
|
|
camera,
|
|
|
|
|
range,
|
|
|
|
|
showPreview,
|
|
|
|
|
setShowPreview,
|
|
|
|
|
}: ExportPreviewDialogProps) {
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation(["components/dialog"]);
|
2024-10-13 20:46:40 +03:00
|
|
|
if (!range) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const source = `${baseUrl}vod/${camera}/start/${range.after}/end/${range.before}/index.m3u8`;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
|
|
|
|
<DialogContent
|
|
|
|
|
className={cn(
|
|
|
|
|
"scrollbar-container overflow-y-auto",
|
|
|
|
|
isDesktop &&
|
|
|
|
|
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl",
|
|
|
|
|
isMobile && "px-4",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<DialogHeader>
|
2025-03-16 18:36:20 +03:00
|
|
|
<DialogTitle>{t("export.fromTimeline.previewExport")}</DialogTitle>
|
2024-10-13 20:46:40 +03:00
|
|
|
<DialogDescription className="sr-only">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("export.fromTimeline.previewExport")}
|
2024-10-13 20:46:40 +03:00
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<GenericVideoPlayer source={source} />
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|