mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-09 15:05:26 +03:00
tweaks
- hide cases when filtering cameras that have no exports from those cameras - remove description from case card - use textarea instead of input for case description in add new case dialog
This commit is contained in:
parent
05913891e1
commit
ae8be19b5b
@ -39,7 +39,10 @@
|
||||
},
|
||||
"deleteCase": {
|
||||
"label": "Delete Case",
|
||||
"desc": "Are you sure you want to delete {{caseName}}? Exports will remain available as uncategorized exports."
|
||||
"desc": "Are you sure you want to delete {{caseName}}?",
|
||||
"descKeepExports": "Exports will remain available as uncategorized exports.",
|
||||
"descDeleteExports": "All exports in this case will be permanently deleted.",
|
||||
"deleteExports": "Also delete exports"
|
||||
},
|
||||
"caseDialog": {
|
||||
"title": "Add to case",
|
||||
|
||||
@ -87,11 +87,11 @@ export function CaseCard({
|
||||
<FaFolder />
|
||||
<div className="truncate smart-capitalize">{exportCase.name}</div>
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-2 text-xs text-white/80">
|
||||
{exports.length === 0
|
||||
? t("caseCard.emptyCase")
|
||||
: exportCase.description}
|
||||
</div>
|
||||
{exports.length === 0 && (
|
||||
<div className="mt-1 text-xs text-white/80">
|
||||
{t("caseCard.emptyCase")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -333,8 +333,8 @@ export function ExportCard({
|
||||
<Skeleton className="absolute inset-0 aspect-video rounded-lg md:rounded-2xl" />
|
||||
)}
|
||||
<ImageShadowOverlay />
|
||||
<div className="absolute bottom-2 left-3 z-30 text-white">
|
||||
<div className="flex items-end smart-capitalize">
|
||||
<div className="absolute bottom-2 left-3 right-12 z-30 text-white">
|
||||
<div className="truncate smart-capitalize">
|
||||
{exportedRecording.name.replaceAll("_", " ")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -133,9 +134,10 @@ export default function OptionAndInputDialog({
|
||||
<label className="text-sm font-medium text-secondary-foreground">
|
||||
{descriptionLabel}
|
||||
</label>
|
||||
<Input
|
||||
<Textarea
|
||||
value={descriptionValue}
|
||||
onChange={(e) => setDescriptionValue(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -23,6 +23,8 @@ import {
|
||||
import Heading from "@/components/ui/heading";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||
@ -83,7 +85,7 @@ function Exports() {
|
||||
|
||||
const { data: cases, mutate: updateCases } = useSWR<ExportCase[]>("cases");
|
||||
const { data: activeExportJobs } = useSWR<ExportJob[]>("jobs/export", {
|
||||
refreshInterval: 2000,
|
||||
refreshInterval: (latestJobs) => ((latestJobs ?? []).length > 0 ? 2000 : 0),
|
||||
});
|
||||
// Keep polling exports while there are queued/running jobs OR while any
|
||||
// existing export is still marked in_progress. Without the second clause,
|
||||
@ -153,8 +155,21 @@ function Exports() {
|
||||
}, [rawExports]);
|
||||
|
||||
const filteredCases = useMemo<ExportCase[]>(() => {
|
||||
return cases || [];
|
||||
}, [cases]);
|
||||
if (!cases) return [];
|
||||
|
||||
const hasCameraFilter =
|
||||
exportFilter?.cameras && exportFilter.cameras.length > 0;
|
||||
|
||||
if (!hasCameraFilter) return cases;
|
||||
|
||||
// When a camera filter is active, hide cases that have zero exports
|
||||
// and zero active jobs matching the filter — they're just noise.
|
||||
return cases.filter(
|
||||
(c) =>
|
||||
(exportsByCase[c.id]?.length ?? 0) > 0 ||
|
||||
(activeJobsByCase[c.id]?.length ?? 0) > 0,
|
||||
);
|
||||
}, [activeJobsByCase, cases, exportFilter?.cameras, exportsByCase]);
|
||||
|
||||
const exports = useMemo<Export[]>(
|
||||
() => exportsByCase["none"] || [],
|
||||
@ -217,6 +232,7 @@ function Exports() {
|
||||
{ mode: "create" | "edit"; exportCase?: ExportCase } | undefined
|
||||
>();
|
||||
const [caseToDelete, setCaseToDelete] = useState<ExportCase | undefined>();
|
||||
const [deleteExportsWithCase, setDeleteExportsWithCase] = useState(false);
|
||||
const [caseForAddExport, setCaseForAddExport] = useState<
|
||||
ExportCase | undefined
|
||||
>();
|
||||
@ -333,11 +349,14 @@ function Exports() {
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.delete(`cases/${caseToDelete.id}`);
|
||||
await axios.delete(`cases/${caseToDelete.id}`, {
|
||||
params: deleteExportsWithCase ? { delete_exports: true } : undefined,
|
||||
});
|
||||
if (selectedCaseId === caseToDelete.id) {
|
||||
setSelectedCaseId(undefined);
|
||||
}
|
||||
setCaseToDelete(undefined);
|
||||
setDeleteExportsWithCase(false);
|
||||
mutate();
|
||||
} catch (error) {
|
||||
const apiError = error as {
|
||||
@ -351,7 +370,7 @@ function Exports() {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
}, [caseToDelete, mutate, selectedCaseId, t]);
|
||||
}, [caseToDelete, deleteExportsWithCase, mutate, selectedCaseId, t]);
|
||||
|
||||
const handleAssignExportToCase = useCallback(
|
||||
async (exportId: string, caseId: string) => {
|
||||
@ -457,7 +476,12 @@ function Exports() {
|
||||
|
||||
<AlertDialog
|
||||
open={caseToDelete != undefined}
|
||||
onOpenChange={() => setCaseToDelete(undefined)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCaseToDelete(undefined);
|
||||
setDeleteExportsWithCase(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@ -465,9 +489,25 @@ function Exports() {
|
||||
<AlertDialogDescription>
|
||||
{t("deleteCase.desc", {
|
||||
caseName: caseToDelete?.name,
|
||||
})}
|
||||
})}{" "}
|
||||
{deleteExportsWithCase
|
||||
? t("deleteCase.descDeleteExports")
|
||||
: t("deleteCase.descKeepExports")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="flex items-center justify-start gap-6">
|
||||
<Label
|
||||
htmlFor="delete-exports-switch"
|
||||
className="cursor-pointer text-sm"
|
||||
>
|
||||
{t("deleteCase.deleteExports")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="delete-exports-switch"
|
||||
checked={deleteExportsWithCase}
|
||||
onCheckedChange={setDeleteExportsWithCase}
|
||||
/>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
@ -526,7 +566,7 @@ function Exports() {
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-col items-start space-y-2 pr-2 md:mb-2 lg:relative lg:h-10 lg:flex-row lg:items-center lg:space-y-0",
|
||||
"flex w-full flex-col items-start space-y-2 md:mb-2 lg:relative lg:h-10 lg:flex-row lg:items-center lg:space-y-0",
|
||||
isMobileOnly && "mb-2 h-auto flex-wrap gap-2 space-y-0",
|
||||
)}
|
||||
>
|
||||
@ -574,32 +614,49 @@ function Exports() {
|
||||
)}
|
||||
{selectedCase && (
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
size="sm"
|
||||
onClick={() => setCaseForAddExport(selectedCase)}
|
||||
>
|
||||
<LuPlus className="size-4 text-secondary-foreground" />
|
||||
<div className="text-primary">{t("toolbar.addExport")}</div>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setCaseDialog({ mode: "edit", exportCase: selectedCase })
|
||||
}
|
||||
>
|
||||
<LuPencil className="size-4 text-secondary-foreground" />
|
||||
<div className="text-primary">{t("toolbar.editCase")}</div>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
size="sm"
|
||||
onClick={() => setCaseToDelete(selectedCase)}
|
||||
>
|
||||
<LuTrash2 className="size-4 text-secondary-foreground" />
|
||||
<div className="text-primary">{t("toolbar.deleteCase")}</div>
|
||||
</Button>
|
||||
<ExportFilterGroup
|
||||
className="justify-start"
|
||||
filter={exportFilter}
|
||||
filters={["cameras"]}
|
||||
onUpdateFilter={setExportFilter}
|
||||
/>
|
||||
<div className="flex items-center gap-1 md:gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
size="sm"
|
||||
aria-label={t("toolbar.addExport")}
|
||||
onClick={() => setCaseForAddExport(selectedCase)}
|
||||
>
|
||||
<LuPlus className="size-4 text-secondary-foreground" />
|
||||
{!isMobile && (
|
||||
<div className="text-primary">{t("toolbar.addExport")}</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
size="sm"
|
||||
aria-label={t("toolbar.editCase")}
|
||||
onClick={() =>
|
||||
setCaseDialog({ mode: "edit", exportCase: selectedCase })
|
||||
}
|
||||
>
|
||||
<LuPencil className="size-4 text-secondary-foreground" />
|
||||
{!isMobile && (
|
||||
<div className="text-primary">{t("toolbar.editCase")}</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
size="sm"
|
||||
aria-label={t("toolbar.deleteCase")}
|
||||
onClick={() => setCaseToDelete(selectedCase)}
|
||||
>
|
||||
<LuTrash2 className="size-4 text-secondary-foreground" />
|
||||
{!isMobile && (
|
||||
<div className="text-primary">{t("toolbar.deleteCase")}</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user