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 } from "react-icons/lu"; import { TimeRange } from "@/types/timeline"; import { ExportContent, ExportPreviewDialog } 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 { isIOS, isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; type DrawerMode = | "none" | "select" | "export" | "calendar" | "filter" | "debug-replay"; const DRAWER_FEATURES = [ "export", "calendar", "filter", "debug-replay", ] as const; export type DrawerFeatures = (typeof DRAWER_FEATURES)[number]; const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [ "export", "calendar", "filter", "debug-replay", ]; 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; 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 = () => {}, onUpdateFilter, setRange, setMode, setShowExportPreview, }: MobileReviewSettingsDrawerProps) { const { t } = useTranslation([ "views/recording", "components/dialog", "views/replay", ]); const navigate = useNavigate(); const [drawerMode, setDrawerMode] = useState("none"); const [selectedReplayOption, setSelectedReplayOption] = useState< "1" | "4" | "8" | "timeline" | "custom" >("1"); const [isDebugReplayStarting, setIsDebugReplayStarting] = useState(false); // exports const [name, setName] = useState(""); const [selectedCaseId, setSelectedCaseId] = useState( undefined, ); const onStartExport = useCallback(() => { if (!range) { toast.error(t("toast.error.noValidTimeSelected"), { position: "top-center", }); return; } if (range.before < range.after) { toast.error(t("toast.error.endTimeMustAfterStartTime"), { position: "top-center", }); return; } axios .post( `export/${camera}/start/${Math.round(range.after)}/end/${Math.round(range.before)}`, { playback: "realtime", name, export_case_id: selectedCaseId || undefined, }, ) .then((response) => { if (response.status == 200) { toast.success( t("export.toast.success", { ns: "components/dialog" }), { position: "top-center", action: ( ), }, ); setName(""); setSelectedCaseId(undefined); setRange(undefined); setMode("none"); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("export.toast.error.failed", { ns: "components/dialog", errorMessage, }), { position: "top-center", }, ); }); }, [camera, name, range, selectedCaseId, setRange, setName, 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("calendar") && ( )} {features.includes("filter") && ( )} {features.includes("debug-replay") && ( )}
); } else if (drawerMode == "export") { content = ( { setMode(mode); if (mode == "timeline") { setDrawerMode("none"); } }} onCancel={() => { setMode("none"); setRange(undefined); setSelectedCaseId(undefined); 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" | "4" | "8" | "timeline" | "custom", ) => { setSelectedReplayOption(option); if (option === "custom" || option === "timeline") { return; } const hours = parseInt(option); const end = latestTime; const now = new Date(end * 1000); now.setHours(now.getHours() - hours); setDebugReplayRange({ after: now.getTime() / 1000, before: end }); }; content = ( { setDebugReplayMode("none"); setDebugReplayRange(undefined); setDrawerMode("select"); }} setRange={setDebugReplayRange} setMode={(mode) => { setDebugReplayMode(mode); if (mode == "timeline") { setDrawerMode("none"); } }} /> ); } return ( <> onStartExport()} onCancel={() => setMode("none")} onPreview={() => setShowExportPreview(true)} /> { setDebugReplayMode("none"); setDebugReplayRange(undefined); }} /> { if (!open) { setDrawerMode("none"); } }} > {content} ); }