mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 09:37:37 +03:00
Add proper filtering and display of cases
This commit is contained in:
parent
46e488653b
commit
7b4f747b6a
@ -15,12 +15,19 @@ import Heading from "@/components/ui/heading";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DeleteClipType, Export, ExportCase } from "@/types/export";
|
import { DeleteClipType, Export, ExportCase } from "@/types/export";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
|
MutableRefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -55,35 +62,12 @@ function Exports() {
|
|||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const filteredCases = useMemo(() => {
|
|
||||||
if (!search || !cases) {
|
|
||||||
return cases;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cases.filter(
|
|
||||||
(caseItem) =>
|
|
||||||
caseItem.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(caseItem.description &&
|
|
||||||
caseItem.description.toLowerCase().includes(search.toLowerCase())),
|
|
||||||
);
|
|
||||||
}, [search, cases]);
|
|
||||||
|
|
||||||
const filteredExports = useMemo<Export[]>(() => {
|
|
||||||
if (!search) {
|
|
||||||
return exports;
|
|
||||||
}
|
|
||||||
|
|
||||||
return exports.filter((exp) =>
|
|
||||||
exp.name
|
|
||||||
.toLowerCase()
|
|
||||||
.replaceAll("_", " ")
|
|
||||||
.includes(search.toLowerCase()),
|
|
||||||
);
|
|
||||||
}, [exports, search]);
|
|
||||||
|
|
||||||
// Viewing
|
// Viewing
|
||||||
|
|
||||||
const [selected, setSelected] = useState<Export>();
|
const [selected, setSelected] = useState<Export>();
|
||||||
|
const [selectedCaseId, setSelectedCaseId] = useOverlayState<
|
||||||
|
string | undefined
|
||||||
|
>("caseId", undefined);
|
||||||
const [selectedAspect, setSelectedAspect] = useState(0.0);
|
const [selectedAspect, setSelectedAspect] = useState(0.0);
|
||||||
|
|
||||||
useSearchEffect("id", (id) => {
|
useSearchEffect("id", (id) => {
|
||||||
@ -95,7 +79,22 @@ function Exports() {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deleting
|
useSearchEffect("caseId", (caseId: string) => {
|
||||||
|
if (!cases) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = cases.some((c) => c.id === caseId);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCaseId(caseId);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modifying
|
||||||
|
|
||||||
const [deleteClip, setDeleteClip] = useState<DeleteClipType | undefined>();
|
const [deleteClip, setDeleteClip] = useState<DeleteClipType | undefined>();
|
||||||
|
|
||||||
@ -112,8 +111,6 @@ function Exports() {
|
|||||||
});
|
});
|
||||||
}, [deleteClip, mutate]);
|
}, [deleteClip, mutate]);
|
||||||
|
|
||||||
// Renaming
|
|
||||||
|
|
||||||
const onHandleRename = useCallback(
|
const onHandleRename = useCallback(
|
||||||
(id: string, update: string) => {
|
(id: string, update: string) => {
|
||||||
axios
|
axios
|
||||||
@ -136,7 +133,7 @@ function Exports() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[mutate, t],
|
[mutate, setDeleteClip, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keyboard Listener
|
// Keyboard Listener
|
||||||
@ -144,6 +141,11 @@ function Exports() {
|
|||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
useKeyboardListener([], undefined, contentRef);
|
useKeyboardListener([], undefined, contentRef);
|
||||||
|
|
||||||
|
const selectedCase = useMemo(
|
||||||
|
() => cases?.find((c) => c.id === selectedCaseId),
|
||||||
|
[cases, selectedCaseId],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col gap-2 overflow-hidden px-1 pt-2 md:p-2">
|
<div className="flex size-full flex-col gap-2 overflow-hidden px-1 pt-2 md:p-2">
|
||||||
<Toaster closeButton={true} />
|
<Toaster closeButton={true} />
|
||||||
@ -227,6 +229,83 @@ function Exports() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedCase ? (
|
||||||
|
<CaseView
|
||||||
|
contentRef={contentRef}
|
||||||
|
selectedCase={selectedCase}
|
||||||
|
exports={rawExports}
|
||||||
|
search={search}
|
||||||
|
setSelected={setSelected}
|
||||||
|
renameClip={onHandleRename}
|
||||||
|
setDeleteClip={setDeleteClip}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AllExportsView
|
||||||
|
contentRef={contentRef}
|
||||||
|
search={search}
|
||||||
|
cases={cases}
|
||||||
|
exports={exports}
|
||||||
|
setSelectedCaseId={setSelectedCaseId}
|
||||||
|
setSelected={setSelected}
|
||||||
|
renameClip={onHandleRename}
|
||||||
|
setDeleteClip={setDeleteClip}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllExportsViewProps = {
|
||||||
|
contentRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
search: string;
|
||||||
|
cases?: ExportCase[];
|
||||||
|
exports: Export[];
|
||||||
|
setSelectedCaseId: (id: string) => void;
|
||||||
|
setSelected: (e: Export) => void;
|
||||||
|
renameClip: (id: string, update: string) => void;
|
||||||
|
setDeleteClip: (d: DeleteClipType | undefined) => void;
|
||||||
|
};
|
||||||
|
function AllExportsView({
|
||||||
|
contentRef,
|
||||||
|
search,
|
||||||
|
cases,
|
||||||
|
exports,
|
||||||
|
setSelectedCaseId,
|
||||||
|
setSelected,
|
||||||
|
renameClip,
|
||||||
|
setDeleteClip,
|
||||||
|
}: AllExportsViewProps) {
|
||||||
|
const { t } = useTranslation(["views/exports"]);
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
|
||||||
|
const filteredCases = useMemo(() => {
|
||||||
|
if (!search || !cases) {
|
||||||
|
return cases;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cases.filter(
|
||||||
|
(caseItem) =>
|
||||||
|
caseItem.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
(caseItem.description &&
|
||||||
|
caseItem.description.toLowerCase().includes(search.toLowerCase())),
|
||||||
|
);
|
||||||
|
}, [search, cases]);
|
||||||
|
|
||||||
|
const filteredExports = useMemo<Export[]>(() => {
|
||||||
|
if (!search) {
|
||||||
|
return exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exports.filter((exp) =>
|
||||||
|
exp.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll("_", " ")
|
||||||
|
.includes(search.toLowerCase()),
|
||||||
|
);
|
||||||
|
}, [exports, search]);
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="w-full overflow-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
{filteredCases?.length || filteredExports.length ? (
|
{filteredCases?.length || filteredExports.length ? (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@ -246,7 +325,9 @@ function Exports() {
|
|||||||
: "hidden"
|
: "hidden"
|
||||||
}
|
}
|
||||||
exportCase={item}
|
exportCase={item}
|
||||||
onSelect={() => {}}
|
onSelect={() => {
|
||||||
|
setSelectedCaseId(item.id);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -269,7 +350,7 @@ function Exports() {
|
|||||||
}
|
}
|
||||||
exportedRecording={item}
|
exportedRecording={item}
|
||||||
onSelect={setSelected}
|
onSelect={setSelected}
|
||||||
onRename={onHandleRename}
|
onRename={renameClip}
|
||||||
onDelete={({ file, exportName }) =>
|
onDelete={({ file, exportName }) =>
|
||||||
setDeleteClip({ file, exportName })
|
setDeleteClip({ file, exportName })
|
||||||
}
|
}
|
||||||
@ -285,6 +366,71 @@ function Exports() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaseViewProps = {
|
||||||
|
contentRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
selectedCase: ExportCase;
|
||||||
|
exports?: Export[];
|
||||||
|
search: string;
|
||||||
|
setSelected: (e: Export) => void;
|
||||||
|
renameClip: (id: string, update: string) => void;
|
||||||
|
setDeleteClip: (d: DeleteClipType | undefined) => void;
|
||||||
|
};
|
||||||
|
function CaseView({
|
||||||
|
contentRef,
|
||||||
|
selectedCase,
|
||||||
|
exports,
|
||||||
|
search,
|
||||||
|
setSelected,
|
||||||
|
renameClip,
|
||||||
|
setDeleteClip,
|
||||||
|
}: CaseViewProps) {
|
||||||
|
const filteredExports = useMemo<Export[]>(() => {
|
||||||
|
const caseExports = (exports || []).filter(
|
||||||
|
(e) => e.export_case == selectedCase.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!search) {
|
||||||
|
return caseExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
return caseExports.filter((exp) =>
|
||||||
|
exp.name
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll("_", " ")
|
||||||
|
.includes(search.toLowerCase()),
|
||||||
|
);
|
||||||
|
}, [selectedCase, exports, search]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Heading className="capitalize" as="h2">
|
||||||
|
{selectedCase.name}
|
||||||
|
</Heading>
|
||||||
|
<div className="text-secondary-foreground">
|
||||||
|
{selectedCase.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={contentRef}
|
||||||
|
className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||||
|
>
|
||||||
|
{exports.map((item) => (
|
||||||
|
<ExportCard
|
||||||
|
key={item.name}
|
||||||
|
className={filteredExports.includes(item) ? "" : "hidden"}
|
||||||
|
exportedRecording={item}
|
||||||
|
onSelect={setSelected}
|
||||||
|
onRename={renameClip}
|
||||||
|
onDelete={({ file, exportName }) =>
|
||||||
|
setDeleteClip({ file, exportName })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user