diff --git a/frigate/http.py b/frigate/http.py index ea7054e3c..ca73f76c0 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -143,7 +143,13 @@ def set_retain(id): def send_to_plus(id): if current_app.plus_api is None: return make_response( - jsonify({"success": False, "message": "Plus token not set"}), 400 + jsonify( + { + "success": False, + "message": "PLUS_API_KEY environment variable is not set", + } + ), + 400, ) try: @@ -182,7 +188,7 @@ def send_to_plus(id): event.plus_id = plus_id event.save() - return "success" + return make_response(jsonify({"success": True, "plus_id": plus_id}), 200) @bp.route("/events//retain", methods=("DELETE",)) @@ -201,6 +207,7 @@ def delete_retain(id): jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200 ) + @bp.route("/events//sub_label", methods=("POST",)) def set_sub_label(id): try: @@ -215,19 +222,31 @@ def set_sub_label(id): else: new_sub_label = None - if new_sub_label and len(new_sub_label) > 20: return make_response( - jsonify({"success": False, "message": new_sub_label + " exceeds the 20 character limit for sub_label"}), 400 + jsonify( + { + "success": False, + "message": new_sub_label + + " exceeds the 20 character limit for sub_label", + } + ), + 400, ) - event.sub_label = new_sub_label event.save() return make_response( - jsonify({"success": True, "message": "Event " + id + " sub label set to " + new_sub_label}), 200 + jsonify( + { + "success": True, + "message": "Event " + id + " sub label set to " + new_sub_label, + } + ), + 200, ) + @bp.route("/events/", methods=("DELETE",)) def delete_event(id): try: diff --git a/web/src/AppBar.jsx b/web/src/AppBar.jsx index 6ff65252e..09fc2e9d8 100644 --- a/web/src/AppBar.jsx +++ b/web/src/AppBar.jsx @@ -6,7 +6,7 @@ import AutoAwesomeIcon from './icons/AutoAwesome'; import LightModeIcon from './icons/LightMode'; import DarkModeIcon from './icons/DarkMode'; import FrigateRestartIcon from './icons/FrigateRestart'; -import Dialog from './components/Dialog'; +import Prompt from './components/Prompt'; import { useDarkMode } from './context'; import { useCallback, useRef, useState } from 'preact/hooks'; import { useRestart } from './api/mqtt'; @@ -65,7 +65,7 @@ export default function AppBar() { ) : null} {showDialog ? ( - ) : null} {showDialogWait ? ( - diff --git a/web/src/icons/UploadPlus.jsx b/web/src/icons/UploadPlus.jsx new file mode 100644 index 000000000..7fd2f884a --- /dev/null +++ b/web/src/icons/UploadPlus.jsx @@ -0,0 +1,23 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function UploadPlus({ className = 'h-6 w-6', stroke = 'currentColor', onClick = () => {} }) { + return ( + + + + ); +} + +export default memo(UploadPlus); diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 94a96debb..84937a2a3 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -10,6 +10,7 @@ import { useState, useRef, useCallback, useMemo } from 'preact/hooks'; import VideoPlayer from '../components/VideoPlayer'; import { StarRecording } from '../icons/StarRecording'; import { Snapshot } from '../icons/Snapshot'; +import { UploadPlus } from '../icons/UploadPlus'; import { Clip } from '../icons/Clip'; import { Zone } from '../icons/Zone'; import { Camera } from '../icons/Camera'; @@ -18,6 +19,8 @@ import { Download } from '../icons/Download'; import Menu, { MenuItem } from '../components/Menu'; import CalendarIcon from '../icons/Calendar'; import Calendar from '../components/Calendar'; +import Button from '../components/Button'; +import Dialog from '../components/Dialog'; const API_LIMIT = 25; @@ -43,10 +46,12 @@ export default function Events({ path, ...props }) { zone: props.zone ?? 'all', }); const [state, setState] = useState({ - showDownloadMenu: null, - showDatePicker: null, - showCalendar: null, + showDownloadMenu: false, + showDatePicker: false, + showCalendar: false, + showPlusConfig: false, }); + const [uploading, setUploading] = useState([]); const [viewEvent, setViewEvent] = useState(); const [downloadEvent, setDownloadEvent] = useState({ id: null, has_clip: false, has_snapshot: false }); @@ -167,6 +172,38 @@ export default function Events({ path, ...props }) { [size, setSize, isValidating, isDone] ); + const onSendToPlus = async (id, e) => { + if (e) { + e.stopPropagation(); + } + + if (!config.plus.enabled) { + setState({ ...state, showDownloadMenu: false, showPlusConfig: true }); + return; + } + + setUploading((prev) => [...prev, id]); + + const response = await axios.post(`events/${id}/plus`); + + if (response.status === 200) { + mutate( + (pages) => + pages.map((page) => + page.map((event) => { + if (event.id === id) { + return { ...event, plus_id: response.data.plus_id }; + } + return event; + }) + ), + false + ); + } + + setUploading((prev) => prev.filter((i) => i !== id)); + }; + if (!config) { return ; } @@ -238,6 +275,14 @@ export default function Events({ path, ...props }) { download /> )} + {downloadEvent.has_snapshot && !downloadEvent.plus_id && ( + onSendToPlus(downloadEvent.id)} + /> + )} )} {state.showDatePicker && ( @@ -282,6 +327,27 @@ export default function Events({ path, ...props }) { /> )} + {state.showPlusConfig && ( + +
+ Setup a Frigate+ Account +

In order to submit images to Frigate+, you first need to setup an account.

+ + https://plus.frigate.video + +
+
+ +
+
+ )}
{eventPages ? ( eventPages.map((page, i) => { @@ -315,7 +381,8 @@ export default function Events({ path, ...props }) {
- {event.sub_label ? `${event.label}: ${event.sub_label}` : event.label} ({(event.top_score * 100).toFixed(0)}%) + {event.sub_label ? `${event.label}: ${event.sub_label}` : event.label} ( + {(event.top_score * 100).toFixed(0)}%)
{new Date(event.start_time * 1000).toLocaleDateString()}{' '} @@ -330,6 +397,19 @@ export default function Events({ path, ...props }) { {event.zones.join(',')}
+
onDelete(e, event.id)} />