import { useCallback, useState } from "react"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Button } from "../ui/button"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; import { LuBug, LuShare2 } from "react-icons/lu"; import { TimeRange } from "@/types/timeline"; import { ExportContent, ExportPreviewDialog, ExportTab } from "./ExportDialog"; import { DebugReplayContent, SaveDebugReplayOverlay, } from "./DebugReplayDialog"; import { ExportMode, GeneralFilter } from "@/types/filter"; import ReviewActivityCalendar from "./ReviewActivityCalendar"; import { SelectSeparator } from "../ui/select"; import { RecordingsSummary, ReviewFilter, ReviewSeverity, ReviewSummary, } from "@/types/review"; import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { GeneralFilterContent } from "../filter/ReviewFilterGroup"; import { toast } from "sonner"; import axios, { AxiosError } from "axios"; import SaveExportOverlay from "./SaveExportOverlay"; import { isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { StartExportResponse } from "@/types/export"; import { ShareTimestampContent } from "./ShareTimestampDialog"; type DrawerMode = | "none" | "select" | "export" | "calendar" | "filter" | "debug-replay" | "share-timestamp"; const DRAWER_FEATURES = [ "export", "calendar", "filter", "debug-replay", "share-timestamp", ] as const; export type DrawerFeatures = (typeof DRAWER_FEATURES)[number]; const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [ "export", "calendar", "filter", "debug-replay", "share-timestamp", ]; type MobileReviewSettingsDrawerProps = { features?: DrawerFeatures[]; camera: string; filter?: ReviewFilter; currentSeverity?: ReviewSeverity; latestTime: number; currentTime: number; range?: TimeRange; mode: ExportMode; showExportPreview: boolean; reviewSummary?: ReviewSummary; recordingsSummary?: RecordingsSummary; allLabels: string[]; allZones: string[]; debugReplayMode?: ExportMode; debugReplayRange?: TimeRange; setDebugReplayMode?: (mode: ExportMode) => void; setDebugReplayRange?: (range: TimeRange | undefined) => void; onShareTimestamp?: (timestamp: number) => void; onUpdateFilter: (filter: ReviewFilter) => void; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; setShowExportPreview: (showPreview: boolean) => void; }; export default function MobileReviewSettingsDrawer({ features = DEFAULT_DRAWER_FEATURES, camera, filter, currentSeverity, latestTime, currentTime, range, mode, showExportPreview, reviewSummary, recordingsSummary, allLabels, allZones, debugReplayMode = "none", debugReplayRange, setDebugReplayMode = () => {}, setDebugReplayRange = () => {}, onShareTimestamp = () => {}, onUpdateFilter, setRange, setMode, setShowExportPreview, }: MobileReviewSettingsDrawerProps) { const { t } = useTranslation([ "views/recording", "components/dialog", "views/replay", "common", ]); const navigate = useNavigate(); const [drawerMode, setDrawerMode] = useState("none"); const [exportTab, setExportTab] = useState("export"); const [selectedReplayOption, setSelectedReplayOption] = useState< "1" | "5" | "custom" | "timeline" >("1"); const [isDebugReplayStarting, setIsDebugReplayStarting] = useState(false); const [selectedShareOption, setSelectedShareOption] = useState< "current" | "custom" >("current"); const [shareTimestampAtOpen, setShareTimestampAtOpen] = useState( Math.floor(currentTime), ); const [customShareTimestamp, setCustomShareTimestamp] = useState( Math.floor(currentTime), ); // exports const [name, setName] = useState(""); const [selectedCaseId, setSelectedCaseId] = useState( undefined, ); const [singleNewCaseName, setSingleNewCaseName] = useState(""); const [singleNewCaseDescription, setSingleNewCaseDescription] = useState(""); const [isStartingExport, setIsStartingExport] = useState(false); const onStartExport = useCallback(async () => { if (isStartingExport) { return false; } if (!range) { toast.error( t("export.toast.error.noVaildTimeSelected", { ns: "components/dialog", }), { position: "top-center", }, ); return false; } if (range.before < range.after) { toast.error( t("export.toast.error.endTimeMustAfterStartTime", { ns: "components/dialog", }), { position: "top-center", }, ); return false; } setIsStartingExport(true); try { let exportCaseId: string | undefined = selectedCaseId; if (selectedCaseId === "new" && singleNewCaseName.trim().length > 0) { const caseResp = await axios.post("cases", { name: singleNewCaseName.trim(), description: singleNewCaseDescription.trim() || undefined, }); exportCaseId = caseResp.data?.id; } else if (selectedCaseId === "new" || selectedCaseId === "none") { exportCaseId = undefined; } await axios.post( `export/${camera}/start/${Math.round(range.after)}/end/${Math.round(range.before)}`, { source: "recordings", name, export_case_id: exportCaseId, }, ); toast.success(t("export.toast.queued", { ns: "components/dialog" }), { position: "top-center", action: ( ), }); setName(""); setSelectedCaseId(undefined); setSingleNewCaseName(""); setSingleNewCaseDescription(""); setRange(undefined); setMode("none"); return true; } catch (error) { const apiError = error as { response?: { data?: { message?: string; detail?: string } }; }; const errorMessage = apiError.response?.data?.message || apiError.response?.data?.detail || "Unknown error"; toast.error( t("export.toast.error.failed", { ns: "components/dialog", error: errorMessage, }), { position: "top-center", }, ); return false; } finally { setIsStartingExport(false); } }, [ camera, isStartingExport, name, range, selectedCaseId, singleNewCaseDescription, singleNewCaseName, setRange, setMode, t, ]); const onStartDebugReplay = useCallback(async () => { if ( !debugReplayRange || debugReplayRange.before <= debugReplayRange.after ) { toast.error( t("dialog.toast.error", { error: "End time must be after start time", ns: "views/replay", }), { position: "top-center" }, ); return; } setIsDebugReplayStarting(true); try { const response = await axios.post("debug_replay/start", { camera: camera, start_time: debugReplayRange.after, end_time: debugReplayRange.before, }); if (response.status === 200) { toast.success(t("dialog.toast.success", { ns: "views/replay" }), { position: "top-center", }); setDebugReplayMode("none"); setDebugReplayRange(undefined); setDrawerMode("none"); navigate("/replay"); } } catch (error) { const axiosError = error as AxiosError<{ message?: string; detail?: string; }>; const errorMessage = axiosError.response?.data?.message || axiosError.response?.data?.detail || "Unknown error"; if (axiosError.response?.status === 409) { toast.error(t("dialog.toast.alreadyActive", { ns: "views/replay" }), { position: "top-center", }); } else { toast.error( t("dialog.toast.error", { error: errorMessage, ns: "views/replay", }), { position: "top-center", }, ); } } finally { setIsDebugReplayStarting(false); } }, [ camera, debugReplayRange, navigate, setDebugReplayMode, setDebugReplayRange, t, ]); // filters const [currentFilter, setCurrentFilter] = useState({ labels: filter?.labels, zones: filter?.zones, showAll: filter?.showAll, ...filter, }); if (!isMobile) { return; } let content; if (drawerMode == "select") { content = (
{features.includes("export") && ( )} {features.includes("share-timestamp") && ( )} {features.includes("calendar") && ( )} {features.includes("filter") && ( )} {features.includes("debug-replay") && ( )}
); } else if (drawerMode == "export") { content = ( { setMode(mode); if (mode == "timeline" || mode == "timeline_multi") { setDrawerMode("none"); } }} onCancel={() => { setMode("none"); setRange(undefined); setSelectedCaseId(undefined); setSingleNewCaseName(""); setSingleNewCaseDescription(""); setExportTab("export"); setDrawerMode("select"); }} /> ); } else if (drawerMode == "calendar") { content = (
setDrawerMode("select")} > {t("button.back", { ns: "common" })}
{t("calendar")}
{ onUpdateFilter({ ...filter, after: day == undefined ? undefined : day.getTime() / 1000, before: day == undefined ? undefined : getEndOfDayTimestamp(day), }); }} />
); } else if (drawerMode == "filter") { content = (
setDrawerMode("select")} > {t("button.back", { ns: "common" })}
{t("filter")}
{ if (currentFilter !== filter) { onUpdateFilter(currentFilter); } }} onReset={() => { const resetFilter: GeneralFilter = {}; setCurrentFilter(resetFilter); onUpdateFilter(resetFilter); }} onClose={() => setDrawerMode("select")} />
); } else if (drawerMode == "debug-replay") { const handleTimeOptionChange = ( option: "1" | "5" | "custom" | "timeline", ) => { setSelectedReplayOption(option); if (option === "custom" || option === "timeline") { return; } const minutes = parseInt(option, 10); const end = latestTime; setDebugReplayRange({ after: end - minutes * 60, before: end }); }; content = ( { setDebugReplayMode("none"); setDebugReplayRange(undefined); setDrawerMode("select"); }} setRange={setDebugReplayRange} setMode={(mode) => { setDebugReplayMode(mode); if (mode == "timeline") { setDrawerMode("none"); } }} /> ); } else if (drawerMode == "share-timestamp") { content = (
{t("recording.shareTimestamp.title", { ns: "components/dialog" })}
{ onShareTimestamp(timestamp); setDrawerMode("none"); }} onCancel={() => setDrawerMode("select")} />
); } return ( <> { if (mode == "timeline_multi") { setExportTab("multi"); setDrawerMode("export"); setMode("select"); return; } void onStartExport(); }} onCancel={() => { setExportTab("export"); setRange(undefined); setMode("none"); }} onPreview={() => setShowExportPreview(true)} /> { setDebugReplayMode("none"); setDebugReplayRange(undefined); }} /> { if (!open) { setDrawerMode("none"); } }} > {content} ); }