import ActivityIndicator from "../indicators/activity-indicator"; import { Button } from "../ui/button"; import { useCallback, useMemo, useRef, useState } from "react"; import { isMobile } from "react-device-detect"; import { FiMoreVertical } from "react-icons/fi"; import { Skeleton } from "../ui/skeleton"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle, } from "../ui/dialog"; import { Input } from "../ui/input"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { DeleteClipType, Export, ExportCase, ExportJob } from "@/types/export"; import { baseUrl } from "@/api/baseUrl"; import { cn } from "@/lib/utils"; import { shareOrCopy } from "@/utils/browserUtil"; import { useTranslation } from "react-i18next"; import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay"; import BlurredIconButton from "../button/BlurredIconButton"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { FaFolder, FaVideo } from "react-icons/fa"; import { HiSquare2Stack } from "react-icons/hi2"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import useContextMenu from "@/hooks/use-contextmenu"; type CaseCardProps = { className: string; exportCase: ExportCase; exports: Export[]; onSelect: () => void; }; export function CaseCard({ className, exportCase, exports, onSelect, }: CaseCardProps) { const { t } = useTranslation(["views/exports"]); const firstExport = useMemo( () => exports.find((exp) => exp.thumb_path && exp.thumb_path.length > 0), [exports], ); const cameraCount = useMemo( () => new Set(exports.map((exp) => exp.camera)).size, [exports], ); return (
onSelect()} > {firstExport && ( )} {!firstExport && (
)}
{exports.length}
{cameraCount}
{exportCase.name}
{exports.length === 0 && (
{t("caseCard.emptyCase")}
)}
); } type ExportCardProps = { className: string; exportedRecording: Export; isSelected?: boolean; selectionMode?: boolean; onSelect: (selected: Export) => void; onContextSelect?: (selected: Export) => void; onRename: (original: string, update: string) => void; onDelete: ({ file, exportName }: DeleteClipType) => void; onAssignToCase?: (selected: Export) => void; onRemoveFromCase?: (selected: Export) => void; }; export function ExportCard({ className, exportedRecording, isSelected, selectionMode, onSelect, onContextSelect, onRename, onDelete, onAssignToCase, onRemoveFromCase, }: ExportCardProps) { const { t } = useTranslation(["views/exports"]); const isAdmin = useIsAdmin(); const [loading, setLoading] = useState( exportedRecording.thumb_path.length > 0, ); // selection const cardRef = useRef(null); useContextMenu(cardRef, () => { if (!exportedRecording.in_progress && onContextSelect) { onContextSelect(exportedRecording); } }); // editing name const [editName, setEditName] = useState<{ original: string; update?: string; }>(); const submitRename = useCallback(() => { if (editName == undefined) { return; } onRename(exportedRecording.id, editName.update ?? ""); setEditName(undefined); }, [editName, exportedRecording, onRename, setEditName]); useKeyboardListener( editName != undefined ? ["Enter"] : [], (key, modifiers) => { if ( key == "Enter" && modifiers.down && !modifiers.repeat && editName && (editName.update?.length ?? 0) > 0 ) { submitRename(); return true; } return false; }, ); return ( <> { if (!open) { setEditName(undefined); } }} > { if (isMobile) { e.preventDefault(); } }} > {t("editExport.title")} {t("editExport.desc")} {editName && ( <> setEditName({ original: editName.original ?? "", update: e.target.value, }) } /> )}
{ if (!exportedRecording.in_progress) { if ((selectionMode || e.ctrlKey || e.metaKey) && onContextSelect) { onContextSelect(exportedRecording); } else { onSelect(exportedRecording); } } }} > {exportedRecording.in_progress ? ( ) : ( <> {exportedRecording.thumb_path.length > 0 ? ( setLoading(false)} /> ) : (
)} )} {!exportedRecording.in_progress && !selectionMode && (
e.stopPropagation()} > { e.stopPropagation(); shareOrCopy( `${baseUrl}export?id=${exportedRecording.id}`, exportedRecording.name.replaceAll("_", " "), ); }} > {t("tooltip.shareExport")} e.stopPropagation()} > {t("tooltip.downloadVideo")} {isAdmin && onAssignToCase && ( { e.stopPropagation(); onAssignToCase(exportedRecording); }} > {t("tooltip.assignToCase")} )} {isAdmin && onRemoveFromCase && ( { e.stopPropagation(); onRemoveFromCase(exportedRecording); }} > {t("tooltip.removeFromCase")} )} {isAdmin && ( { e.stopPropagation(); setEditName({ original: exportedRecording.name, update: undefined, }); }} > {t("tooltip.editName")} )} {isAdmin && ( { e.stopPropagation(); onDelete({ file: exportedRecording.id, exportName: exportedRecording.name, }); }} > {t("tooltip.deleteExport")} )}
)} {loading && ( )}
{exportedRecording.name.replaceAll("_", " ")}
); } type ActiveExportJobCardProps = { className?: string; job: ExportJob; }; export function ActiveExportJobCard({ className = "", job, }: ActiveExportJobCardProps) { const { t } = useTranslation(["views/exports", "common"]); const cameraName = useCameraFriendlyName(job.camera); const displayName = useMemo(() => { if (job.name && job.name.length > 0) { return job.name.replaceAll("_", " "); } return t("jobCard.defaultName", { camera: cameraName, }); }, [cameraName, job.name, t]); const statusLabel = job.status === "queued" ? t("jobCard.queued") : t("jobCard.running"); return (
{statusLabel}
{displayName}
); }