From 26744efb1e797418e36bcb2ad806862ecfa1ba50 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 ++- web/src/pages/Exports.tsx | 4 ++ 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json index a56c2b1da..907a61add 100644 --- a/web/public/locales/en/components/dialog.json +++ b/web/public/locales/en/components/dialog.json @@ -48,6 +48,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 976b20042..5d68d88db 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"); }} /> diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx index 5b4a3ef3a..2f4cbd53d 100644 --- a/web/src/pages/Exports.tsx +++ b/web/src/pages/Exports.tsx @@ -321,6 +321,7 @@ function Exports() { search={search} cases={filteredCases} exports={exports} + exportsByCase={exportsByCase} setSelectedCaseId={setSelectedCaseId} setSelected={setSelected} renameClip={onHandleRename} @@ -337,6 +338,7 @@ type AllExportsViewProps = { search: string; cases?: ExportCase[]; exports: Export[]; + exportsByCase: { [caseId: string]: Export[] }; setSelectedCaseId: (id: string) => void; setSelected: (e: Export) => void; renameClip: (id: string, update: string) => void; @@ -348,6 +350,7 @@ function AllExportsView({ search, cases, exports, + exportsByCase, setSelectedCaseId, setSelected, renameClip, @@ -404,6 +407,7 @@ function AllExportsView({ : "hidden" } exportCase={item} + exports={exportsByCase[item.id] || []} onSelect={() => { setSelectedCaseId(item.id); }}