From e5664826b1f4459b78c6df2ad858844f43c3dcf7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 21 Sep 2023 04:20:57 -0600 Subject: [PATCH] Add ability to play and delete exports from webUI (#7882) * add ability to playback exports on exports screen * Add ability to delete exports from exports screen * Fix large dialog * Formatting --- frigate/http.py | 15 +++++ web/src/components/DialogLarge.jsx | 35 ++++++++++++ web/src/routes/Export.jsx | 91 ++++++++++++++++++++++++++++-- 3 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 web/src/components/DialogLarge.jsx diff --git a/frigate/http.py b/frigate/http.py index d26a7bd2a..4354e205e 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -34,6 +34,7 @@ from frigate.const import ( CACHE_DIR, CLIPS_DIR, CONFIG_DIR, + EXPORT_DIR, MAX_SEGMENT_DURATION, RECORD_DIR, ) @@ -1666,6 +1667,20 @@ def export_recording(camera_name: str, start_time, end_time): return "Starting export of recording", 200 +@bp.route("/export/", methods=["DELETE"]) +def export_delete(file_name: str): + file = os.path.join(EXPORT_DIR, file_name) + + if not os.path.exists(file): + return make_response( + jsonify({"success": False, "message": f"{file_name} not found."}), + 404, + ) + + os.unlink(file) + return "Successfully deleted file", 200 + + def imagestream(detected_frames_processor, camera_name, fps, height, draw_options): while True: # max out at specified FPS diff --git a/web/src/components/DialogLarge.jsx b/web/src/components/DialogLarge.jsx new file mode 100644 index 000000000..73ebf063f --- /dev/null +++ b/web/src/components/DialogLarge.jsx @@ -0,0 +1,35 @@ +import { h, Fragment } from 'preact'; +import { createPortal } from 'preact/compat'; +import { useState, useEffect } from 'preact/hooks'; + +export default function LargeDialog({ children, portalRootID = 'dialogs' }) { + const portalRoot = portalRootID && document.getElementById(portalRootID); + const [show, setShow] = useState(false); + + useEffect(() => { + window.requestAnimationFrame(() => { + setShow(true); + }); + }, []); + + const dialog = ( + +
+
+ {children} +
+
+
+ ); + + return portalRoot ? createPortal(dialog, portalRoot) : dialog; +} diff --git a/web/src/routes/Export.jsx b/web/src/routes/Export.jsx index 7156a1b0e..d942e6a32 100644 --- a/web/src/routes/Export.jsx +++ b/web/src/routes/Export.jsx @@ -1,16 +1,22 @@ import Heading from '../components/Heading'; import { useState } from 'preact/hooks'; -import useSWR from 'swr'; +import useSWR, { mutate } from 'swr'; import Button from '../components/Button'; import axios from 'axios'; import { baseUrl } from '../api/baseUrl'; import { Fragment } from 'preact'; import ActivityIndicator from '../components/ActivityIndicator'; +import { Play } from '../icons/Play'; +import { Delete } from '../icons/Delete'; +import LargeDialog from '../components/DialogLarge'; +import VideoPlayer from '../components/VideoPlayer'; +import Dialog from '../components/Dialog'; export default function Export() { const { data: config } = useSWR('config'); const { data: exports } = useSWR('exports/', (url) => axios({ baseURL: baseUrl, url }).then((res) => res.data)); + // Export States const [camera, setCamera] = useState('select'); const [playback, setPlayback] = useState('select'); const [message, setMessage] = useState({ text: '', error: false }); @@ -26,6 +32,11 @@ export default function Export() { const [endDate, setEndDate] = useState(localISODate); const [endTime, setEndTime] = useState('23:59'); + // Export States + + const [selectedClip, setSelectedClip] = useState(); + const [deleteClip, setDeleteClip] = useState(); + const onHandleExport = () => { if (camera == 'select') { setMessage({ text: 'A camera needs to be selected.', error: true }); @@ -66,6 +77,15 @@ export default function Export() { }); }; + const onHandleDelete = (clip) => { + axios.delete(`export/${clip}`).then((response) => { + if (response.status == 200) { + setDeleteClip(); + mutate(); + } + }); + }; + return (
Export @@ -74,6 +94,55 @@ export default function Export() {
{message.text}
)} + {selectedClip && ( + +
+ Playback + { + this.player = player; + }} + onDispose={() => { + this.player = null; + }} + /> +
+
+ +
+
+ )} + + {deleteClip && ( + +
+ Delete Export? +

Confirm deletion of {deleteClip}.

+
+
+ + +
+
+ )} +
@@ -144,7 +213,11 @@ export default function Export() { {exports && (
Exports - + setSelectedClip(clip)} + onDeleteClip={(clip) => setDeleteClip(clip)} + />
)}
@@ -152,7 +225,7 @@ export default function Export() { ); } -function Exports({ exports }) { +function Exports({ exports, onSetClip, onDeleteClip }) { return ( {exports.map((item) => ( @@ -166,9 +239,19 @@ function Exports({ exports }) {
) : (
- + + {item.name.substring(0, item.name.length - 4)} +
)}