mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
add frontend for frigate+ submission
This commit is contained in:
parent
e724fe3da6
commit
cef77fba01
@ -143,7 +143,13 @@ def set_retain(id):
|
|||||||
def send_to_plus(id):
|
def send_to_plus(id):
|
||||||
if current_app.plus_api is None:
|
if current_app.plus_api is None:
|
||||||
return make_response(
|
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:
|
try:
|
||||||
@ -182,7 +188,7 @@ def send_to_plus(id):
|
|||||||
event.plus_id = plus_id
|
event.plus_id = plus_id
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
return "success"
|
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/events/<id>/retain", methods=("DELETE",))
|
@bp.route("/events/<id>/retain", methods=("DELETE",))
|
||||||
@ -201,6 +207,7 @@ def delete_retain(id):
|
|||||||
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
|
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/events/<id>/sub_label", methods=("POST",))
|
@bp.route("/events/<id>/sub_label", methods=("POST",))
|
||||||
def set_sub_label(id):
|
def set_sub_label(id):
|
||||||
try:
|
try:
|
||||||
@ -215,19 +222,31 @@ def set_sub_label(id):
|
|||||||
else:
|
else:
|
||||||
new_sub_label = None
|
new_sub_label = None
|
||||||
|
|
||||||
|
|
||||||
if new_sub_label and len(new_sub_label) > 20:
|
if new_sub_label and len(new_sub_label) > 20:
|
||||||
return make_response(
|
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.sub_label = new_sub_label
|
||||||
event.save()
|
event.save()
|
||||||
return make_response(
|
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/<id>", methods=("DELETE",))
|
@bp.route("/events/<id>", methods=("DELETE",))
|
||||||
def delete_event(id):
|
def delete_event(id):
|
||||||
try:
|
try:
|
||||||
|
@ -6,7 +6,7 @@ import AutoAwesomeIcon from './icons/AutoAwesome';
|
|||||||
import LightModeIcon from './icons/LightMode';
|
import LightModeIcon from './icons/LightMode';
|
||||||
import DarkModeIcon from './icons/DarkMode';
|
import DarkModeIcon from './icons/DarkMode';
|
||||||
import FrigateRestartIcon from './icons/FrigateRestart';
|
import FrigateRestartIcon from './icons/FrigateRestart';
|
||||||
import Dialog from './components/Dialog';
|
import Prompt from './components/Prompt';
|
||||||
import { useDarkMode } from './context';
|
import { useDarkMode } from './context';
|
||||||
import { useCallback, useRef, useState } from 'preact/hooks';
|
import { useCallback, useRef, useState } from 'preact/hooks';
|
||||||
import { useRestart } from './api/mqtt';
|
import { useRestart } from './api/mqtt';
|
||||||
@ -65,7 +65,7 @@ export default function AppBar() {
|
|||||||
</Menu>
|
</Menu>
|
||||||
) : null}
|
) : null}
|
||||||
{showDialog ? (
|
{showDialog ? (
|
||||||
<Dialog
|
<Prompt
|
||||||
onDismiss={handleDismissRestartDialog}
|
onDismiss={handleDismissRestartDialog}
|
||||||
title="Restart Frigate"
|
title="Restart Frigate"
|
||||||
text="Are you sure?"
|
text="Are you sure?"
|
||||||
@ -76,7 +76,7 @@ export default function AppBar() {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{showDialogWait ? (
|
{showDialogWait ? (
|
||||||
<Dialog
|
<Prompt
|
||||||
title="Restart in progress"
|
title="Restart in progress"
|
||||||
text="Please wait a few seconds for the restart to complete before reloading the page."
|
text="Please wait a few seconds for the restart to complete before reloading the page."
|
||||||
/>
|
/>
|
||||||
|
23
web/src/icons/UploadPlus.jsx
Normal file
23
web/src/icons/UploadPlus.jsx
Normal file
@ -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 (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke={stroke}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(UploadPlus);
|
@ -10,6 +10,7 @@ import { useState, useRef, useCallback, useMemo } from 'preact/hooks';
|
|||||||
import VideoPlayer from '../components/VideoPlayer';
|
import VideoPlayer from '../components/VideoPlayer';
|
||||||
import { StarRecording } from '../icons/StarRecording';
|
import { StarRecording } from '../icons/StarRecording';
|
||||||
import { Snapshot } from '../icons/Snapshot';
|
import { Snapshot } from '../icons/Snapshot';
|
||||||
|
import { UploadPlus } from '../icons/UploadPlus';
|
||||||
import { Clip } from '../icons/Clip';
|
import { Clip } from '../icons/Clip';
|
||||||
import { Zone } from '../icons/Zone';
|
import { Zone } from '../icons/Zone';
|
||||||
import { Camera } from '../icons/Camera';
|
import { Camera } from '../icons/Camera';
|
||||||
@ -18,6 +19,8 @@ import { Download } from '../icons/Download';
|
|||||||
import Menu, { MenuItem } from '../components/Menu';
|
import Menu, { MenuItem } from '../components/Menu';
|
||||||
import CalendarIcon from '../icons/Calendar';
|
import CalendarIcon from '../icons/Calendar';
|
||||||
import Calendar from '../components/Calendar';
|
import Calendar from '../components/Calendar';
|
||||||
|
import Button from '../components/Button';
|
||||||
|
import Dialog from '../components/Dialog';
|
||||||
|
|
||||||
const API_LIMIT = 25;
|
const API_LIMIT = 25;
|
||||||
|
|
||||||
@ -43,10 +46,12 @@ export default function Events({ path, ...props }) {
|
|||||||
zone: props.zone ?? 'all',
|
zone: props.zone ?? 'all',
|
||||||
});
|
});
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
showDownloadMenu: null,
|
showDownloadMenu: false,
|
||||||
showDatePicker: null,
|
showDatePicker: false,
|
||||||
showCalendar: null,
|
showCalendar: false,
|
||||||
|
showPlusConfig: false,
|
||||||
});
|
});
|
||||||
|
const [uploading, setUploading] = useState([]);
|
||||||
const [viewEvent, setViewEvent] = useState();
|
const [viewEvent, setViewEvent] = useState();
|
||||||
const [downloadEvent, setDownloadEvent] = useState({ id: null, has_clip: false, has_snapshot: false });
|
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]
|
[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) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -238,6 +275,14 @@ export default function Events({ path, ...props }) {
|
|||||||
download
|
download
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{downloadEvent.has_snapshot && !downloadEvent.plus_id && (
|
||||||
|
<MenuItem
|
||||||
|
icon={UploadPlus}
|
||||||
|
label="Send to Frigate+"
|
||||||
|
value="plus"
|
||||||
|
onSelect={() => onSendToPlus(downloadEvent.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
{state.showDatePicker && (
|
{state.showDatePicker && (
|
||||||
@ -282,6 +327,27 @@ export default function Events({ path, ...props }) {
|
|||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
|
{state.showPlusConfig && (
|
||||||
|
<Dialog>
|
||||||
|
<div className="p-4">
|
||||||
|
<Heading size="lg">Setup a Frigate+ Account</Heading>
|
||||||
|
<p className="mb-2">In order to submit images to Frigate+, you first need to setup an account.</p>
|
||||||
|
<a
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
href="https://plus.frigate.video"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
https://plus.frigate.video
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
||||||
|
<Button className="ml-2" onClick={() => setState({ ...state, showPlusConfig: false })} type="text">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{eventPages ? (
|
{eventPages ? (
|
||||||
eventPages.map((page, i) => {
|
eventPages.map((page, i) => {
|
||||||
@ -315,7 +381,8 @@ export default function Events({ path, ...props }) {
|
|||||||
<div className="m-2 flex grow">
|
<div className="m-2 flex grow">
|
||||||
<div className="flex flex-col grow">
|
<div className="flex flex-col grow">
|
||||||
<div className="capitalize text-lg font-bold">
|
<div className="capitalize text-lg font-bold">
|
||||||
{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)}%)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
|
{new Date(event.start_time * 1000).toLocaleDateString()}{' '}
|
||||||
@ -330,6 +397,19 @@ export default function Events({ path, ...props }) {
|
|||||||
{event.zones.join(',')}
|
{event.zones.join(',')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hidden sm:flex flex-col justify-end mr-2">
|
||||||
|
{event.plus_id ? (
|
||||||
|
<div className="uppercase text-xs">Sent to Frigate+</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
color="gray"
|
||||||
|
disabled={uploading.includes(event.id)}
|
||||||
|
onClick={(e) => onSendToPlus(event.id, e)}
|
||||||
|
>
|
||||||
|
{uploading.includes(event.id) ? 'Uploading...' : 'Send to Frigate+'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
|
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user