mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +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): | ||||
|     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/<id>/retain", methods=("DELETE",)) | ||||
| @ -201,6 +207,7 @@ def delete_retain(id): | ||||
|         jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200 | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @bp.route("/events/<id>/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/<id>", methods=("DELETE",)) | ||||
| def delete_event(id): | ||||
|     try: | ||||
|  | ||||
| @ -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() { | ||||
|         </Menu> | ||||
|       ) : null} | ||||
|       {showDialog ? ( | ||||
|         <Dialog | ||||
|         <Prompt | ||||
|           onDismiss={handleDismissRestartDialog} | ||||
|           title="Restart Frigate" | ||||
|           text="Are you sure?" | ||||
| @ -76,7 +76,7 @@ export default function AppBar() { | ||||
|         /> | ||||
|       ) : null} | ||||
|       {showDialogWait ? ( | ||||
|         <Dialog | ||||
|         <Prompt | ||||
|           title="Restart in progress" | ||||
|           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 { 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 <ActivityIndicator />; | ||||
|   } | ||||
| @ -238,6 +275,14 @@ export default function Events({ path, ...props }) { | ||||
|               download | ||||
|             /> | ||||
|           )} | ||||
|           {downloadEvent.has_snapshot && !downloadEvent.plus_id && ( | ||||
|             <MenuItem | ||||
|               icon={UploadPlus} | ||||
|               label="Send to Frigate+" | ||||
|               value="plus" | ||||
|               onSelect={() => onSendToPlus(downloadEvent.id)} | ||||
|             /> | ||||
|           )} | ||||
|         </Menu> | ||||
|       )} | ||||
|       {state.showDatePicker && ( | ||||
| @ -282,6 +327,27 @@ export default function Events({ path, ...props }) { | ||||
|           /> | ||||
|         </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"> | ||||
|         {eventPages ? ( | ||||
|           eventPages.map((page, i) => { | ||||
| @ -315,7 +381,8 @@ export default function Events({ path, ...props }) { | ||||
|                     <div className="m-2 flex grow"> | ||||
|                       <div className="flex flex-col grow"> | ||||
|                         <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 className="text-sm"> | ||||
|                           {new Date(event.start_time * 1000).toLocaleDateString()}{' '} | ||||
| @ -330,6 +397,19 @@ export default function Events({ path, ...props }) { | ||||
|                           {event.zones.join(',')} | ||||
|                         </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"> | ||||
|                         <Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} /> | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user