From 476a900708592870d0ec0f9974b0535a4b6bb4de Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 3 Apr 2024 08:02:07 -0600 Subject: [PATCH] Add ability to rename exports (#10791) * Add ability to rename exports * Address feedback --- frigate/record/export.py | 2 +- web/src/components/card/ExportCard.tsx | 198 ++++++++++++++++++------- web/src/pages/Export.tsx | 15 +- 3 files changed, 156 insertions(+), 59 deletions(-) diff --git a/frigate/record/export.py b/frigate/record/export.py index f5861d4f7..2a5b46fa3 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -61,7 +61,7 @@ class RecordingExporter(threading.Thread): ) file_name = ( self.user_provided_name - or f"{self.camera}@{self.get_datetime_from_timestamp(self.start_time)}__{self.get_datetime_from_timestamp(self.end_time)}" + or f"{self.camera}_{self.get_datetime_from_timestamp(self.start_time)}__{self.get_datetime_from_timestamp(self.end_time)}" ) file_path = f"{EXPORT_DIR}/in_progress.{file_name}.mp4" final_file_path = f"{EXPORT_DIR}/{file_name}.mp4" diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index 8d5d2eef7..7431781ea 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -1,21 +1,25 @@ import { baseUrl } from "@/api/baseUrl"; import ActivityIndicator from "../indicators/activity-indicator"; -import { LuTrash } from "react-icons/lu"; +import { LuPencil, LuTrash } from "react-icons/lu"; import { Button } from "../ui/button"; import { useMemo, useRef, 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"; type ExportProps = { file: { name: string; }; + onRename: (original: string, update: string) => void; onDelete: (file: string) => void; }; -export default function ExportCard({ file, onDelete }: ExportProps) { +export default function ExportCard({ file, onRename, onDelete }: ExportProps) { const videoRef = useRef(null); const [hovered, setHovered] = useState(false); const [playing, setPlaying] = useState(false); @@ -25,63 +29,143 @@ export default function ExportCard({ file, onDelete }: ExportProps) { [file.name], ); + // editing name + + const [editName, setEditName] = useState<{ + original: string; + update: string; + }>(); + + useKeyboardListener( + editName != undefined ? ["Enter"] : [], + (_, down, repeat) => { + if (down && !repeat && editName && editName.update.length > 0) { + onRename(editName.original, editName.update.replaceAll(" ", "_")); + setEditName(undefined); + } + }, + ); + return ( -
setHovered(true) : undefined - } - onMouseLeave={ - isDesktop && !inProgress ? () => setHovered(false) : undefined - } - onClick={isDesktop || inProgress ? undefined : () => setHovered(!hovered)} - > - {!playing && hovered && ( - <> -
- onDelete(file.name)} + <> + { + if (!open) { + setEditName(undefined); + } + }} + > + + Rename Export + {editName && ( + <> + + setEditName({ + original: editName.original ?? "", + update: e.target.value, + }) + } + /> + + + + + )} + + + +
setHovered(true) : undefined + } + onMouseLeave={ + isDesktop && !inProgress ? () => setHovered(false) : undefined + } + onClick={ + isDesktop || inProgress ? undefined : () => setHovered(!hovered) + } + > + {hovered && ( + <> + {!playing && ( +
+ )} +
+ setEditName({ original: file.name, update: "" })} + > + + + onDelete(file.name)} + > + + +
+ {!playing && ( + + )} + + )} + {inProgress ? ( + + ) : ( +
+ ); } diff --git a/web/src/pages/Export.tsx b/web/src/pages/Export.tsx index ff6f5765f..a5f775419 100644 --- a/web/src/pages/Export.tsx +++ b/web/src/pages/Export.tsx @@ -26,6 +26,18 @@ function Export() { const [deleteClip, setDeleteClip] = useState(); + const onHandleRename = useCallback( + (original: string, update: string) => { + axios.patch(`export/${original}/${update}`).then((response) => { + if (response.status == 200) { + setDeleteClip(undefined); + mutate(); + } + }); + }, + [mutate], + ); + const onHandleDelete = useCallback(() => { if (!deleteClip) { return; @@ -61,13 +73,14 @@ function Export() { -
+
{exports && (
{Object.values(exports).map((item) => ( setDeleteClip(file)} /> ))}