From 1d930fd57bdad53effe486ed5eccddaf1e65e2d5 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 3 Jan 2026 08:03:33 -0700 Subject: [PATCH] Exports Improvements (#21521) * Add images to case folder view * Add ability to select case in export dialog * Add to mobile review too --- web/public/locales/en/components/dialog.json | 4 ++ web/src/components/card/ExportCard.tsx | 27 ++++++-- web/src/components/overlay/ExportDialog.tsx | 63 ++++++++++++++++++- .../overlay/MobileReviewSettingsDrawer.tsx | 10 ++- 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json index 91ff38d82..9a6f68daf 100644 --- a/web/public/locales/en/components/dialog.json +++ b/web/public/locales/en/components/dialog.json @@ -49,6 +49,10 @@ "name": { "placeholder": "Name the Export" }, + "case": { + "label": "Case", + "placeholder": "Select a case" + }, "select": "Select", "export": "Export", "selectOrExport": "Select or Export", diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index fc7964c18..c8d9c4c65 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -1,6 +1,6 @@ import ActivityIndicator from "../indicators/activity-indicator"; import { Button } from "../ui/button"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { isMobile } from "react-device-detect"; import { FiMoreVertical } from "react-icons/fi"; import { Skeleton } from "../ui/skeleton"; @@ -32,18 +32,37 @@ import { FaFolder } from "react-icons/fa"; type CaseCardProps = { className: string; exportCase: ExportCase; + exports: Export[]; onSelect: () => void; }; -export function CaseCard({ className, exportCase, onSelect }: CaseCardProps) { +export function CaseCard({ + className, + exportCase, + exports, + onSelect, +}: CaseCardProps) { + const firstExport = useMemo( + () => exports.find((exp) => exp.thumb_path && exp.thumb_path.length > 0), + [exports], + ); + return (
onSelect()} > -
+ {firstExport && ( + + )} +
+
{exportCase.name}
diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index b8b5b9911..738aa689e 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -22,7 +22,14 @@ import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { TimezoneAwareCalendar } from "./ReviewActivityCalendar"; -import { SelectSeparator } from "../ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "../ui/select"; import { isDesktop, isIOS, isMobile } from "react-device-detect"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import SaveExportOverlay from "./SaveExportOverlay"; @@ -31,6 +38,7 @@ import { baseUrl } from "@/api/baseUrl"; import { cn } from "@/lib/utils"; import { GenericVideoPlayer } from "../player/GenericVideoPlayer"; import { useTranslation } from "react-i18next"; +import { ExportCase } from "@/types/export"; const EXPORT_OPTIONS = [ "1", @@ -67,6 +75,9 @@ export default function ExportDialog({ }: ExportDialogProps) { const { t } = useTranslation(["components/dialog"]); const [name, setName] = useState(""); + const [selectedCaseId, setSelectedCaseId] = useState( + undefined, + ); const onStartExport = useCallback(() => { if (!range) { @@ -89,6 +100,7 @@ export default function ExportDialog({ { playback: "realtime", name, + export_case_id: selectedCaseId || undefined, }, ) .then((response) => { @@ -102,6 +114,7 @@ export default function ExportDialog({ ), }); setName(""); + setSelectedCaseId(undefined); setRange(undefined); setMode("none"); } @@ -118,10 +131,11 @@ export default function ExportDialog({ { position: "top-center" }, ); }); - }, [camera, name, range, setRange, setName, setMode, t]); + }, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]); const handleCancel = useCallback(() => { setName(""); + setSelectedCaseId(undefined); setMode("none"); setRange(undefined); }, [setMode, setRange]); @@ -190,8 +204,10 @@ export default function ExportDialog({ currentTime={currentTime} range={range} name={name} + selectedCaseId={selectedCaseId} onStartExport={onStartExport} setName={setName} + setSelectedCaseId={setSelectedCaseId} setRange={setRange} setMode={setMode} onCancel={handleCancel} @@ -207,8 +223,10 @@ type ExportContentProps = { currentTime: number; range?: TimeRange; name: string; + selectedCaseId?: string; onStartExport: () => void; setName: (name: string) => void; + setSelectedCaseId: (caseId: string | undefined) => void; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; onCancel: () => void; @@ -218,14 +236,17 @@ export function ExportContent({ currentTime, range, name, + selectedCaseId, onStartExport, setName, + setSelectedCaseId, setRange, setMode, onCancel, }: ExportContentProps) { const { t } = useTranslation(["components/dialog"]); const [selectedOption, setSelectedOption] = useState("1"); + const { data: cases } = useSWR("cases"); const onSelectTime = useCallback( (option: ExportOption) => { @@ -320,6 +341,44 @@ export function ExportContent({ value={name} onChange={(e) => setName(e.target.value)} /> +
+ + +
{isDesktop && } ( + undefined, + ); const onStartExport = useCallback(() => { if (!range) { toast.error(t("toast.error.noValidTimeSelected"), { @@ -96,6 +99,7 @@ export default function MobileReviewSettingsDrawer({ { playback: "realtime", name, + export_case_id: selectedCaseId || undefined, }, ) .then((response) => { @@ -114,6 +118,7 @@ export default function MobileReviewSettingsDrawer({ }, ); setName(""); + setSelectedCaseId(undefined); setRange(undefined); setMode("none"); } @@ -133,7 +138,7 @@ export default function MobileReviewSettingsDrawer({ }, ); }); - }, [camera, name, range, setRange, setName, setMode, t]); + }, [camera, name, range, selectedCaseId, setRange, setName, setMode, t]); // filters @@ -200,8 +205,10 @@ export default function MobileReviewSettingsDrawer({ currentTime={currentTime} range={range} name={name} + selectedCaseId={selectedCaseId} onStartExport={onStartExport} setName={setName} + setSelectedCaseId={setSelectedCaseId} setRange={setRange} setMode={(mode) => { setMode(mode); @@ -213,6 +220,7 @@ export default function MobileReviewSettingsDrawer({ onCancel={() => { setMode("none"); setRange(undefined); + setSelectedCaseId(undefined); setDrawerMode("select"); }} />