frigate/web/src/components/card/ExportCard.tsx

171 lines
5.4 KiB
TypeScript
Raw Normal View History

import ActivityIndicator from "../indicators/activity-indicator";
import { LuPencil, LuTrash } from "react-icons/lu";
import { Button } from "../ui/button";
2024-04-19 17:52:37 +03:00
import { useState } from "react";
import { isDesktop } from "react-device-detect";
import { FaPlay } from "react-icons/fa";
import Chip from "../indicators/Chip";
import { Skeleton } from "../ui/skeleton";
import { Dialog, DialogContent, DialogFooter, DialogTitle } from "../ui/dialog";
import { Input } from "../ui/input";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
2024-04-19 17:42:59 +03:00
import { Export } from "@/types/export";
type ExportProps = {
className: string;
2024-04-19 17:42:59 +03:00
exportedRecording: Export;
2024-04-19 17:52:37 +03:00
onSelect: (selected: Export) => void;
onRename: (original: string, update: string) => void;
onDelete: (file: string) => void;
};
export default function ExportCard({
className,
2024-04-19 17:42:59 +03:00
exportedRecording,
2024-04-19 17:52:37 +03:00
onSelect,
onRename,
onDelete,
}: ExportProps) {
const [hovered, setHovered] = useState(false);
2024-04-19 19:06:08 +03:00
const [loading, setLoading] = useState(
exportedRecording.thumb_path.length > 0,
);
// editing name
const [editName, setEditName] = useState<{
original: string;
update: string;
}>();
useKeyboardListener(
editName != undefined ? ["Enter"] : [],
(_, down, repeat) => {
if (down && !repeat && editName && editName.update.length > 0) {
2024-04-19 18:02:43 +03:00
onRename(exportedRecording.id, editName.update);
setEditName(undefined);
}
},
);
return (
<>
<Dialog
open={editName != undefined}
onOpenChange={(open) => {
if (!open) {
setEditName(undefined);
}
}}
>
<DialogContent>
<DialogTitle>Rename Export</DialogTitle>
{editName && (
<>
<Input
className="mt-3"
type="search"
placeholder={editName?.original}
value={editName?.update}
onChange={(e) =>
setEditName({
original: editName.original ?? "",
update: e.target.value,
})
}
/>
<DialogFooter>
<Button
size="sm"
variant="select"
disabled={(editName?.update?.length ?? 0) == 0}
onClick={() => {
2024-04-19 18:02:43 +03:00
onRename(exportedRecording.id, editName.update);
setEditName(undefined);
}}
>
Save
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
<div
className={`relative aspect-video bg-black rounded-2xl flex justify-center items-center ${className}`}
onMouseEnter={
2024-04-19 17:42:59 +03:00
isDesktop && !exportedRecording.in_progress
? () => setHovered(true)
: undefined
}
onMouseLeave={
2024-04-19 17:42:59 +03:00
isDesktop && !exportedRecording.in_progress
? () => setHovered(false)
: undefined
}
onClick={
2024-04-19 17:42:59 +03:00
isDesktop || exportedRecording.in_progress
? undefined
: () => setHovered(!hovered)
}
>
{hovered && (
<>
2024-04-19 17:52:37 +03:00
<div className="absolute inset-0 z-10 bg-black bg-opacity-60 rounded-2xl" />
<div className="absolute top-1 right-1 flex items-center gap-2">
<Chip
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
2024-04-19 17:42:59 +03:00
onClick={() =>
2024-04-19 18:02:43 +03:00
setEditName({ original: exportedRecording.name, update: "" })
2024-04-19 17:42:59 +03:00
}
>
<LuPencil className="size-4 text-white" />
</Chip>
<Chip
className="bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 rounded-md cursor-pointer"
2024-04-19 17:42:59 +03:00
onClick={() => onDelete(exportedRecording.id)}
>
<LuTrash className="size-4 text-destructive fill-destructive" />
</Chip>
</div>
2024-04-19 17:52:37 +03:00
<Button
2024-04-19 19:06:08 +03:00
className="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 w-20 h-20 z-20 text-white hover:text-white hover:bg-transparent cursor-pointer"
2024-04-19 17:52:37 +03:00
variant="ghost"
onClick={() => {
onSelect(exportedRecording);
}}
>
<FaPlay />
</Button>
</>
)}
2024-04-19 17:42:59 +03:00
{exportedRecording.in_progress ? (
<ActivityIndicator />
) : (
2024-04-19 19:06:08 +03:00
<>
{exportedRecording.thumb_path.length > 0 ? (
<img
className="size-full absolute inset-0 object-contain aspect-video rounded-2xl"
src={exportedRecording.thumb_path.replace("/media/frigate", "")}
onLoad={() => setLoading(false)}
/>
) : (
<div className="absolute inset-0 bg-secondary rounded-2xl" />
)}
</>
)}
{loading && (
<Skeleton className="absolute inset-0 aspect-video rounded-2xl" />
)}
2024-04-19 17:52:37 +03:00
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none rounded-2xl">
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm capitalize">
{exportedRecording.name}
</div>
2024-04-19 17:52:37 +03:00
</div>
</div>
</>
);
}