mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	UI Fixes (#11742)
* Allow deleting failed in progress exports * Fix comparison and preview retrieval * Fix stretching of event cards * Reset edit state when group changes * Allow specifying group
This commit is contained in:
		
							parent
							
								
									7917bf55ff
								
							
						
					
					
						commit
						2875e84cb5
					
				| @ -4,6 +4,7 @@ import logging | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Optional | from typing import Optional | ||||||
| 
 | 
 | ||||||
|  | import psutil | ||||||
| from flask import ( | from flask import ( | ||||||
|     Blueprint, |     Blueprint, | ||||||
|     current_app, |     current_app, | ||||||
| @ -14,6 +15,7 @@ from flask import ( | |||||||
| from peewee import DoesNotExist | from peewee import DoesNotExist | ||||||
| from werkzeug.utils import secure_filename | from werkzeug.utils import secure_filename | ||||||
| 
 | 
 | ||||||
|  | from frigate.const import EXPORT_DIR | ||||||
| from frigate.models import Export, Recordings | from frigate.models import Export, Recordings | ||||||
| from frigate.record.export import PlaybackFactorEnum, RecordingExporter | from frigate.record.export import PlaybackFactorEnum, RecordingExporter | ||||||
| 
 | 
 | ||||||
| @ -140,6 +142,27 @@ def export_delete(id: str): | |||||||
|             404, |             404, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |     files_in_use = [] | ||||||
|  |     for process in psutil.process_iter(): | ||||||
|  |         try: | ||||||
|  |             if process.name() != "ffmpeg": | ||||||
|  |                 continue | ||||||
|  |             flist = process.open_files() | ||||||
|  |             if flist: | ||||||
|  |                 for nt in flist: | ||||||
|  |                     if nt.path.startswith(EXPORT_DIR): | ||||||
|  |                         files_in_use.append(nt.path.split("/")[-1]) | ||||||
|  |         except psutil.Error: | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |     if export.video_path.split("/")[-1] in files_in_use: | ||||||
|  |         return make_response( | ||||||
|  |             jsonify( | ||||||
|  |                 {"success": False, "message": "Can not delete in progress export."} | ||||||
|  |             ), | ||||||
|  |             400, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|     Path(export.video_path).unlink(missing_ok=True) |     Path(export.video_path).unlink(missing_ok=True) | ||||||
| 
 | 
 | ||||||
|     if export.thumb_path: |     if export.thumb_path: | ||||||
|  | |||||||
| @ -11,6 +11,8 @@ import threading | |||||||
| from enum import Enum | from enum import Enum | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| 
 | 
 | ||||||
|  | from peewee import DoesNotExist | ||||||
|  | 
 | ||||||
| from frigate.config import FrigateConfig | from frigate.config import FrigateConfig | ||||||
| from frigate.const import ( | from frigate.const import ( | ||||||
|     CACHE_DIR, |     CACHE_DIR, | ||||||
| @ -72,30 +74,32 @@ class RecordingExporter(threading.Thread): | |||||||
| 
 | 
 | ||||||
|         if datetime.datetime.fromtimestamp( |         if datetime.datetime.fromtimestamp( | ||||||
|             self.start_time |             self.start_time | ||||||
|         ) < datetime.datetime.now().replace(minute=0, second=0): |         ) < datetime.datetime.now().astimezone(datetime.timezone.dst).replace( | ||||||
|  |             minute=0, second=0, microsecond=0 | ||||||
|  |         ): | ||||||
|             # has preview mp4 |             # has preview mp4 | ||||||
|             preview: Previews = ( |             try: | ||||||
|                 Previews.select( |                 preview: Previews = ( | ||||||
|                     Previews.camera, |                     Previews.select( | ||||||
|                     Previews.path, |                         Previews.camera, | ||||||
|                     Previews.duration, |                         Previews.path, | ||||||
|                     Previews.start_time, |                         Previews.duration, | ||||||
|                     Previews.end_time, |                         Previews.start_time, | ||||||
|                 ) |                         Previews.end_time, | ||||||
|                 .where( |  | ||||||
|                     Previews.start_time.between(self.start_time, self.end_time) |  | ||||||
|                     | Previews.end_time.between(self.start_time, self.end_time) |  | ||||||
|                     | ( |  | ||||||
|                         (self.start_time > Previews.start_time) |  | ||||||
|                         & (self.end_time < Previews.end_time) |  | ||||||
|                     ) |                     ) | ||||||
|  |                     .where( | ||||||
|  |                         Previews.start_time.between(self.start_time, self.end_time) | ||||||
|  |                         | Previews.end_time.between(self.start_time, self.end_time) | ||||||
|  |                         | ( | ||||||
|  |                             (self.start_time > Previews.start_time) | ||||||
|  |                             & (self.end_time < Previews.end_time) | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                     .where(Previews.camera == self.camera) | ||||||
|  |                     .limit(1) | ||||||
|  |                     .get() | ||||||
|                 ) |                 ) | ||||||
|                 .where(Previews.camera == self.camera) |             except DoesNotExist: | ||||||
|                 .limit(1) |  | ||||||
|                 .get() |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|             if not preview: |  | ||||||
|                 return "" |                 return "" | ||||||
| 
 | 
 | ||||||
|             diff = self.start_time - preview.start_time |             diff = self.start_time - preview.start_time | ||||||
|  | |||||||
| @ -14,8 +14,12 @@ import { baseUrl } from "@/api/baseUrl"; | |||||||
| 
 | 
 | ||||||
| type AnimatedEventCardProps = { | type AnimatedEventCardProps = { | ||||||
|   event: ReviewSegment; |   event: ReviewSegment; | ||||||
|  |   selectedGroup?: string; | ||||||
| }; | }; | ||||||
| export function AnimatedEventCard({ event }: AnimatedEventCardProps) { | export function AnimatedEventCard({ | ||||||
|  |   event, | ||||||
|  |   selectedGroup, | ||||||
|  | }: AnimatedEventCardProps) { | ||||||
|   const { data: config } = useSWR<FrigateConfig>("config"); |   const { data: config } = useSWR<FrigateConfig>("config"); | ||||||
| 
 | 
 | ||||||
|   const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]); |   const currentHour = useMemo(() => isCurrentHour(event.start_time), [event]); | ||||||
| @ -53,7 +57,8 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) { | |||||||
| 
 | 
 | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const onOpenReview = useCallback(() => { |   const onOpenReview = useCallback(() => { | ||||||
|     navigate("review", { |     const url = selectedGroup ? `review?group=${selectedGroup}` : "review"; | ||||||
|  |     navigate(url, { | ||||||
|       state: { |       state: { | ||||||
|         severity: event.severity, |         severity: event.severity, | ||||||
|         recording: { |         recording: { | ||||||
| @ -64,7 +69,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) { | |||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|     axios.post(`reviews/viewed`, { ids: [event.id] }); |     axios.post(`reviews/viewed`, { ids: [event.id] }); | ||||||
|   }, [navigate, event]); |   }, [navigate, selectedGroup, event]); | ||||||
| 
 | 
 | ||||||
|   // image behavior
 |   // image behavior
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -109,43 +109,38 @@ export default function ExportCard({ | |||||||
|           "relative flex aspect-video items-center justify-center rounded-lg bg-black md:rounded-2xl", |           "relative flex aspect-video items-center justify-center rounded-lg bg-black md:rounded-2xl", | ||||||
|           className, |           className, | ||||||
|         )} |         )} | ||||||
|         onMouseEnter={ |         onMouseEnter={isDesktop ? () => setHovered(true) : undefined} | ||||||
|           isDesktop && !exportedRecording.in_progress |         onMouseLeave={isDesktop ? () => setHovered(false) : undefined} | ||||||
|             ? () => setHovered(true) |         onClick={isDesktop ? undefined : () => setHovered(!hovered)} | ||||||
|             : undefined |  | ||||||
|         } |  | ||||||
|         onMouseLeave={ |  | ||||||
|           isDesktop && !exportedRecording.in_progress |  | ||||||
|             ? () => setHovered(false) |  | ||||||
|             : undefined |  | ||||||
|         } |  | ||||||
|         onClick={ |  | ||||||
|           isDesktop || exportedRecording.in_progress |  | ||||||
|             ? undefined |  | ||||||
|             : () => setHovered(!hovered) |  | ||||||
|         } |  | ||||||
|       > |       > | ||||||
|         {hovered && ( |         {hovered && ( | ||||||
|           <> |           <> | ||||||
|             <div className="absolute inset-0 z-10 rounded-lg bg-black bg-opacity-60 md:rounded-2xl" /> |             <div className="absolute inset-0 z-10 rounded-lg bg-black bg-opacity-60 md:rounded-2xl" /> | ||||||
|             <div className="absolute right-1 top-1 flex items-center gap-2"> |             <div className="absolute right-1 top-1 flex items-center gap-2"> | ||||||
|               <a |               {!exportedRecording.in_progress && ( | ||||||
|                 className="z-20" |                 <a | ||||||
|                 download |                   className="z-20" | ||||||
|                 href={`${baseUrl}${exportedRecording.video_path.replace("/media/frigate/", "")}`} |                   download | ||||||
|               > |                   href={`${baseUrl}${exportedRecording.video_path.replace("/media/frigate/", "")}`} | ||||||
|                 <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"> |                 > | ||||||
|                   <FaDownload className="size-4 text-white" /> |                   <Chip className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"> | ||||||
|  |                     <FaDownload className="size-4 text-white" /> | ||||||
|  |                   </Chip> | ||||||
|  |                 </a> | ||||||
|  |               )} | ||||||
|  |               {!exportedRecording.in_progress && ( | ||||||
|  |                 <Chip | ||||||
|  |                   className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500" | ||||||
|  |                   onClick={() => | ||||||
|  |                     setEditName({ | ||||||
|  |                       original: exportedRecording.name, | ||||||
|  |                       update: "", | ||||||
|  |                     }) | ||||||
|  |                   } | ||||||
|  |                 > | ||||||
|  |                   <MdEditSquare className="size-4 text-white" /> | ||||||
|                 </Chip> |                 </Chip> | ||||||
|               </a> |               )} | ||||||
|               <Chip |  | ||||||
|                 className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500" |  | ||||||
|                 onClick={() => |  | ||||||
|                   setEditName({ original: exportedRecording.name, update: "" }) |  | ||||||
|                 } |  | ||||||
|               > |  | ||||||
|                 <MdEditSquare className="size-4 text-white" /> |  | ||||||
|               </Chip> |  | ||||||
|               <Chip |               <Chip | ||||||
|                 className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500" |                 className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500" | ||||||
|                 onClick={() => |                 onClick={() => | ||||||
| @ -159,15 +154,17 @@ export default function ExportCard({ | |||||||
|               </Chip> |               </Chip> | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|             <Button |             {!exportedRecording.in_progress && ( | ||||||
|               className="absolute left-1/2 top-1/2 z-20 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white" |               <Button | ||||||
|               variant="ghost" |                 className="absolute left-1/2 top-1/2 z-20 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white" | ||||||
|               onClick={() => { |                 variant="ghost" | ||||||
|                 onSelect(exportedRecording); |                 onClick={() => { | ||||||
|               }} |                   onSelect(exportedRecording); | ||||||
|             > |                 }} | ||||||
|               <FaPlay /> |               > | ||||||
|             </Button> |                 <FaPlay /> | ||||||
|  |               </Button> | ||||||
|  |             )} | ||||||
|           </> |           </> | ||||||
|         )} |         )} | ||||||
|         {exportedRecording.in_progress ? ( |         {exportedRecording.in_progress ? ( | ||||||
|  | |||||||
| @ -68,6 +68,19 @@ export default function Events() { | |||||||
|   const [reviewFilter, setReviewFilter, reviewSearchParams] = |   const [reviewFilter, setReviewFilter, reviewSearchParams] = | ||||||
|     useApiFilter<ReviewFilter>(); |     useApiFilter<ReviewFilter>(); | ||||||
| 
 | 
 | ||||||
|  |   useSearchEffect("group", (reviewGroup) => { | ||||||
|  |     if (config && reviewGroup) { | ||||||
|  |       const group = config.camera_groups[reviewGroup]; | ||||||
|  | 
 | ||||||
|  |       if (group) { | ||||||
|  |         setReviewFilter({ | ||||||
|  |           ...reviewFilter, | ||||||
|  |           cameras: group.cameras, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   const onUpdateFilter = useCallback( |   const onUpdateFilter = useCallback( | ||||||
|     (newFilter: ReviewFilter) => { |     (newFilter: ReviewFilter) => { | ||||||
|       setReviewFilter(newFilter); |       setReviewFilter(newFilter); | ||||||
|  | |||||||
| @ -668,27 +668,32 @@ function Timeline({ | |||||||
|           <Skeleton className="size-full" /> |           <Skeleton className="size-full" /> | ||||||
|         ) |         ) | ||||||
|       ) : ( |       ) : ( | ||||||
|         <div |         <div className="h-full overflow-auto bg-secondary"> | ||||||
|           className={`grid h-full grid-cols-1 gap-4 overflow-auto bg-secondary p-4 ${isDesktop ? "" : "sm:grid-cols-2"}`} |           <div | ||||||
|         > |             className={cn( | ||||||
|           {mainCameraReviewItems.map((review) => { |               "grid h-auto grid-cols-1 gap-4 overflow-auto p-4", | ||||||
|             if (review.severity == "significant_motion") { |               isMobile && "sm:grid-cols-2", | ||||||
|               return; |             )} | ||||||
|             } |           > | ||||||
|  |             {mainCameraReviewItems.map((review) => { | ||||||
|  |               if (review.severity == "significant_motion") { | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
| 
 | 
 | ||||||
|             return ( |               return ( | ||||||
|               <ReviewCard |                 <ReviewCard | ||||||
|                 key={review.id} |                   key={review.id} | ||||||
|                 event={review} |                   event={review} | ||||||
|                 currentTime={currentTime} |                   currentTime={currentTime} | ||||||
|                 onClick={() => { |                   onClick={() => { | ||||||
|                   setScrubbing(true); |                     setScrubbing(true); | ||||||
|                   setCurrentTime(review.start_time - REVIEW_PADDING); |                     setCurrentTime(review.start_time - REVIEW_PADDING); | ||||||
|                   setScrubbing(false); |                     setScrubbing(false); | ||||||
|                 }} |                   }} | ||||||
|               /> |                 /> | ||||||
|             ); |               ); | ||||||
|           })} |             })} | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -91,8 +91,16 @@ export default function DraggableGridLayout({ | |||||||
|     ); |     ); | ||||||
|   }, [config]); |   }, [config]); | ||||||
| 
 | 
 | ||||||
|  |   // editing
 | ||||||
|  | 
 | ||||||
|   const [editGroup, setEditGroup] = useState(false); |   const [editGroup, setEditGroup] = useState(false); | ||||||
| 
 | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     setEditGroup(false); | ||||||
|  |   }, [cameraGroup]); | ||||||
|  | 
 | ||||||
|  |   // camera state
 | ||||||
|  | 
 | ||||||
|   const [currentCameras, setCurrentCameras] = useState<CameraConfig[]>(); |   const [currentCameras, setCurrentCameras] = useState<CameraConfig[]>(); | ||||||
|   const [currentIncludeBirdseye, setCurrentIncludeBirdseye] = |   const [currentIncludeBirdseye, setCurrentIncludeBirdseye] = | ||||||
|     useState<boolean>(); |     useState<boolean>(); | ||||||
|  | |||||||
| @ -224,7 +224,13 @@ export default function LiveDashboardView({ | |||||||
|           <TooltipProvider> |           <TooltipProvider> | ||||||
|             <div className="flex items-center gap-2 px-1"> |             <div className="flex items-center gap-2 px-1"> | ||||||
|               {events.map((event) => { |               {events.map((event) => { | ||||||
|                 return <AnimatedEventCard key={event.id} event={event} />; |                 return ( | ||||||
|  |                   <AnimatedEventCard | ||||||
|  |                     key={event.id} | ||||||
|  |                     event={event} | ||||||
|  |                     selectedGroup={cameraGroup} | ||||||
|  |                   /> | ||||||
|  |                 ); | ||||||
|               })} |               })} | ||||||
|             </div> |             </div> | ||||||
|           </TooltipProvider> |           </TooltipProvider> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user