mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 09:37:37 +03:00
Show cases separately from exports
This commit is contained in:
parent
d3a88fc78c
commit
46e488653b
@ -2,6 +2,10 @@
|
|||||||
"documentTitle": "Export - Frigate",
|
"documentTitle": "Export - Frigate",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"noExports": "No exports found",
|
"noExports": "No exports found",
|
||||||
|
"headings": {
|
||||||
|
"cases": "Cases",
|
||||||
|
"uncategorizedExports": "Uncategorized Exports"
|
||||||
|
},
|
||||||
"deleteExport": "Delete Export",
|
"deleteExport": "Delete Export",
|
||||||
"deleteExport.desc": "Are you sure you want to delete {{exportName}}?",
|
"deleteExport.desc": "Are you sure you want to delete {{exportName}}?",
|
||||||
"editExport": {
|
"editExport": {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
} from "../ui/dialog";
|
} from "../ui/dialog";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { DeleteClipType, Export } from "@/types/export";
|
import { DeleteClipType, Export, ExportCase } from "@/types/export";
|
||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { shareOrCopy } from "@/utils/browserUtil";
|
import { shareOrCopy } from "@/utils/browserUtil";
|
||||||
@ -27,22 +27,46 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "../ui/dropdown-menu";
|
} from "../ui/dropdown-menu";
|
||||||
|
import { FaFolder } from "react-icons/fa";
|
||||||
|
|
||||||
type ExportProps = {
|
type CaseCardProps = {
|
||||||
|
className: string;
|
||||||
|
exportCase: ExportCase;
|
||||||
|
onSelect: () => void;
|
||||||
|
};
|
||||||
|
export function CaseCard({ className, exportCase, onSelect }: CaseCardProps) {
|
||||||
|
const { t } = useTranslation(["views/exports"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex aspect-video size-full cursor-pointer items-center justify-center rounded-lg bg-secondary md:rounded-2xl",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={() => onSelect()}
|
||||||
|
>
|
||||||
|
<div className="absolute bottom-2 left-2 flex items-center justify-start gap-2">
|
||||||
|
<FaFolder />
|
||||||
|
<div className="capitalize">{exportCase.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportCardProps = {
|
||||||
className: string;
|
className: string;
|
||||||
exportedRecording: Export;
|
exportedRecording: Export;
|
||||||
onSelect: (selected: Export) => void;
|
onSelect: (selected: Export) => void;
|
||||||
onRename: (original: string, update: string) => void;
|
onRename: (original: string, update: string) => void;
|
||||||
onDelete: ({ file, exportName }: DeleteClipType) => void;
|
onDelete: ({ file, exportName }: DeleteClipType) => void;
|
||||||
};
|
};
|
||||||
|
export function ExportCard({
|
||||||
export default function ExportCard({
|
|
||||||
className,
|
className,
|
||||||
exportedRecording,
|
exportedRecording,
|
||||||
onSelect,
|
onSelect,
|
||||||
onRename,
|
onRename,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: ExportProps) {
|
}: ExportCardProps) {
|
||||||
const { t } = useTranslation(["views/exports"]);
|
const { t } = useTranslation(["views/exports"]);
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
const [loading, setLoading] = useState(
|
const [loading, setLoading] = useState(
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import ExportCard from "@/components/card/ExportCard";
|
import { CaseCard, ExportCard } from "@/components/card/ExportCard";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@ -11,12 +11,13 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
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 { useSearchEffect } from "@/hooks/use-overlay-state";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DeleteClipType, Export } 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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
@ -29,18 +30,46 @@ import useSWR from "swr";
|
|||||||
|
|
||||||
function Exports() {
|
function Exports() {
|
||||||
const { t } = useTranslation(["views/exports"]);
|
const { t } = useTranslation(["views/exports"]);
|
||||||
const { data: exports, mutate } = useSWR<Export[]>("exports");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t("documentTitle");
|
document.title = t("documentTitle");
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
// Data
|
||||||
|
|
||||||
|
const { data: cases, mutate: updateCases } = useSWR<ExportCase[]>("cases");
|
||||||
|
const { data: rawExports, mutate: updateExports } =
|
||||||
|
useSWR<Export[]>("exports");
|
||||||
|
|
||||||
|
const exports = useMemo<Export[]>(
|
||||||
|
() => (rawExports ?? []).filter((e) => !e.export_case),
|
||||||
|
[rawExports],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutate = useCallback(() => {
|
||||||
|
updateExports();
|
||||||
|
updateCases();
|
||||||
|
}, [updateExports, updateCases]);
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const filteredExports = useMemo(() => {
|
const filteredCases = useMemo(() => {
|
||||||
if (!search || !exports) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +216,7 @@ function Exports() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{exports && (
|
{(exports?.length || cases?.length) && (
|
||||||
<div className="flex w-full items-center justify-center p-2">
|
<div className="flex w-full items-center justify-center p-2">
|
||||||
<Input
|
<Input
|
||||||
className="text-md w-full bg-muted md:w-1/3"
|
className="text-md w-full bg-muted md:w-1/3"
|
||||||
@ -199,25 +228,55 @@ function Exports() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-full overflow-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
{exports && filteredExports && filteredExports.length > 0 ? (
|
{filteredCases?.length || filteredExports.length ? (
|
||||||
<div
|
<div className="flex flex-col gap-4">
|
||||||
ref={contentRef}
|
{cases?.length && (
|
||||||
className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
<div className="space-y-2">
|
||||||
>
|
<Heading as="h4">{t("headings.cases")}</Heading>
|
||||||
{Object.values(exports).map((item) => (
|
<div
|
||||||
<ExportCard
|
ref={contentRef}
|
||||||
key={item.name}
|
className="scrollbar-container grid size-full gap-2 overflow-y-auto sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||||
className={
|
>
|
||||||
search == "" || filteredExports.includes(item) ? "" : "hidden"
|
{cases.map((item) => (
|
||||||
}
|
<CaseCard
|
||||||
exportedRecording={item}
|
key={item.name}
|
||||||
onSelect={setSelected}
|
className={
|
||||||
onRename={onHandleRename}
|
search == "" || filteredCases?.includes(item)
|
||||||
onDelete={({ file, exportName }) =>
|
? ""
|
||||||
setDeleteClip({ file, exportName })
|
: "hidden"
|
||||||
}
|
}
|
||||||
/>
|
exportCase={item}
|
||||||
))}
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Heading as="h4">{t("headings.uncategorizedExports")}</Heading>
|
||||||
|
<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={
|
||||||
|
search == "" || filteredExports.includes(item)
|
||||||
|
? ""
|
||||||
|
: "hidden"
|
||||||
|
}
|
||||||
|
exportedRecording={item}
|
||||||
|
onSelect={setSelected}
|
||||||
|
onRename={onHandleRename}
|
||||||
|
onDelete={({ file, exportName }) =>
|
||||||
|
setDeleteClip({ file, exportName })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||||
|
|||||||
@ -6,6 +6,15 @@ export type Export = {
|
|||||||
video_path: string;
|
video_path: string;
|
||||||
thumb_path: string;
|
thumb_path: string;
|
||||||
in_progress: boolean;
|
in_progress: boolean;
|
||||||
|
export_case?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExportCase = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteClipType = {
|
export type DeleteClipType = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user