mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	UI fixes (#9986)
* scroll minimap to keep it in view * remove console log * change ref * rebase to dev * rebase to dev * rebase to dev * fix history flexbox and live extra scrollbar * remove extra class
This commit is contained in:
		
							parent
							
								
									f84d2db406
								
							
						
					
					
						commit
						a6aa5328aa
					
				| @ -1,7 +1,7 @@ | ||||
| import { useEventUtils } from "@/hooks/use-event-utils"; | ||||
| import { useSegmentUtils } from "@/hooks/use-segment-utils"; | ||||
| import { ReviewSegment, ReviewSeverity } from "@/types/review"; | ||||
| import React, { useMemo } from "react"; | ||||
| import React, { useEffect, useMemo, useRef } from "react"; | ||||
| 
 | ||||
| type EventSegmentProps = { | ||||
|   events: ReviewSegment[]; | ||||
| @ -19,6 +19,7 @@ type MinimapSegmentProps = { | ||||
|   isLastSegmentInMinimap: boolean; | ||||
|   alignedMinimapStartTime: number; | ||||
|   alignedMinimapEndTime: number; | ||||
|   firstMinimapSegmentRef: React.MutableRefObject<HTMLDivElement | null>; | ||||
| }; | ||||
| 
 | ||||
| type TickSegmentProps = { | ||||
| @ -41,11 +42,15 @@ function MinimapBounds({ | ||||
|   isLastSegmentInMinimap, | ||||
|   alignedMinimapStartTime, | ||||
|   alignedMinimapEndTime, | ||||
|   firstMinimapSegmentRef, | ||||
| }: MinimapSegmentProps) { | ||||
|   return ( | ||||
|     <> | ||||
|       {isFirstSegmentInMinimap && ( | ||||
|         <div className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px]"> | ||||
|         <div | ||||
|           className="absolute inset-0 -bottom-5 w-full flex items-center justify-center text-xs text-primary font-medium z-20 text-center text-[8px] scroll-mt-8" | ||||
|           ref={firstMinimapSegmentRef} | ||||
|         > | ||||
|           {new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { | ||||
|             hour: "2-digit", | ||||
|             minute: "2-digit", | ||||
| @ -179,6 +184,19 @@ export function EventSegment({ | ||||
|     return showMinimap && segmentTime === alignedMinimapEndTime; | ||||
|   }, [showMinimap, segmentTime, alignedMinimapEndTime]); | ||||
| 
 | ||||
|   const firstMinimapSegmentRef = useRef<HTMLDivElement>(null); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     // Check if the first segment is out of view
 | ||||
|     const firstSegment = firstMinimapSegmentRef.current; | ||||
|     if (firstSegment && showMinimap && isFirstSegmentInMinimap) { | ||||
|       firstSegment.scrollIntoView({ | ||||
|         behavior: "smooth", | ||||
|         block: "center", | ||||
|       }); | ||||
|     } | ||||
|   }, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]); | ||||
| 
 | ||||
|   const segmentClasses = `flex flex-row ${ | ||||
|     showMinimap | ||||
|       ? isInMinimapRange | ||||
| @ -212,6 +230,7 @@ export function EventSegment({ | ||||
|         isLastSegmentInMinimap={isLastSegmentInMinimap} | ||||
|         alignedMinimapStartTime={alignedMinimapStartTime} | ||||
|         alignedMinimapEndTime={alignedMinimapEndTime} | ||||
|         firstMinimapSegmentRef={firstMinimapSegmentRef} | ||||
|       /> | ||||
| 
 | ||||
|       <Tick | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { useCallback } from "react"; | ||||
| import { useCallback, useEffect } from "react"; | ||||
| 
 | ||||
| interface DragHandlerProps { | ||||
|   contentRef: React.RefObject<HTMLElement>; | ||||
| @ -128,6 +128,17 @@ function useDraggableHandler({ | ||||
|     ] | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     // TODO: determine when we want to do this
 | ||||
|     const handlebar = scrollTimeRef.current; | ||||
|     if (handlebar && showHandlebar) { | ||||
|       // handlebar.scrollIntoView({
 | ||||
|       //   behavior: "smooth",
 | ||||
|       //   block: "center",
 | ||||
|       // });
 | ||||
|     } | ||||
|   }, []); | ||||
| 
 | ||||
|   return { handleMouseDown, handleMouseUp, handleMouseMove }; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -79,7 +79,7 @@ function Live() { | ||||
|   }, []); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="w-full h-full overflow-scroll px-2"> | ||||
|     <div className="w-full h-full overflow-y-scroll px-2"> | ||||
|       {events && events.length > 0 && ( | ||||
|         <ScrollArea> | ||||
|           <TooltipProvider> | ||||
|  | ||||
| @ -61,7 +61,8 @@ function eventsToScrubberItems(events: Event[]): ScrubberItem[] { | ||||
| } | ||||
| 
 | ||||
| const generateRandomEvent = (): ReviewSegment => { | ||||
|   const start_time = Math.floor(Date.now() / 1000) - Math.random() * 60 * 60; | ||||
|   const start_time = | ||||
|     Math.floor(Date.now() / 1000) - 10800 - Math.random() * 60 * 60; | ||||
|   const end_time = Math.floor(start_time + Math.random() * 60 * 10); | ||||
|   const severities: ReviewSeverity[] = [ | ||||
|     "significant_motion", | ||||
| @ -123,6 +124,23 @@ function UIPlayground() { | ||||
|     setMockEvents(initialEvents); | ||||
|   }, []); | ||||
| 
 | ||||
|   // Calculate minimap start and end times based on events
 | ||||
|   const minimapStartTime = useMemo(() => { | ||||
|     if (mockEvents && mockEvents.length > 0) { | ||||
|       return Math.min(...mockEvents.map((event) => event.start_time)); | ||||
|     } | ||||
|     return Math.floor(Date.now() / 1000); // Default to current time if no events
 | ||||
|   }, [events]); | ||||
| 
 | ||||
|   const minimapEndTime = useMemo(() => { | ||||
|     if (mockEvents && mockEvents.length > 0) { | ||||
|       return Math.max( | ||||
|         ...mockEvents.map((event) => event.end_time ?? event.start_time) | ||||
|       ); | ||||
|     } | ||||
|     return Math.floor(Date.now() / 1000); // Default to current time if no events
 | ||||
|   }, [events]); | ||||
| 
 | ||||
|   const [zoomLevel, setZoomLevel] = useState(0); | ||||
|   const [zoomSettings, setZoomSettings] = useState({ | ||||
|     segmentDuration: 60, | ||||
| @ -150,101 +168,114 @@ function UIPlayground() { | ||||
|     setZoomSettings(possibleZoomLevels[nextZoomLevel]); | ||||
|   } | ||||
| 
 | ||||
|   const [isDragging, setIsDragging] = useState(false); | ||||
| 
 | ||||
|   const handleDraggingChange = (dragging: boolean) => { | ||||
|     setIsDragging(dragging); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Heading as="h2">UI Playground</Heading> | ||||
|       <div className="w-full h-full"> | ||||
|         <div className="flex h-full"> | ||||
|           <div className="flex-1 content-start gap-2 overflow-y-auto no-scrollbar mt-4 mr-5"> | ||||
|             <Heading as="h2">UI Playground</Heading> | ||||
| 
 | ||||
|       <Heading as="h4" className="my-5"> | ||||
|         Scrubber | ||||
|       </Heading> | ||||
|       <p className="text-small"> | ||||
|         Shows the 10 most recent events within the last 4 hours | ||||
|       </p> | ||||
| 
 | ||||
|       {!config && <ActivityIndicator />} | ||||
| 
 | ||||
|       {config && ( | ||||
|         <div> | ||||
|           {events && events.length > 0 && ( | ||||
|             <> | ||||
|               <ActivityScrubber | ||||
|                 items={eventsToScrubberItems(events)} | ||||
|                 selectHandler={onSelect} | ||||
|               /> | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {config && ( | ||||
|         <div> | ||||
|           {timeline && ( | ||||
|             <> | ||||
|               <TimelineScrubber eventID={timeline} /> | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       <div className="flex"> | ||||
|         <div className="flex-grow"> | ||||
|           <div ref={contentRef}> | ||||
|             <Heading as="h4" className="my-5"> | ||||
|               Timeline | ||||
|             </Heading> | ||||
|             <p className="text-small">Handlebar timestamp: {handlebarTime}</p> | ||||
|             <p> | ||||
|               <Button onClick={handleZoomOut} disabled={zoomLevel === 0}> | ||||
|                 Zoom Out | ||||
|               </Button> | ||||
|               <Button | ||||
|                 onClick={handleZoomIn} | ||||
|                 disabled={zoomLevel === possibleZoomLevels.length - 1} | ||||
|               > | ||||
|                 Zoom In | ||||
|               </Button> | ||||
|             </p> | ||||
|             <Heading as="h4" className="my-5"> | ||||
|               Color scheme | ||||
|               Scrubber | ||||
|             </Heading> | ||||
|             <p className="text-small"> | ||||
|               Colors as set by the current theme. See the{" "} | ||||
|               <a | ||||
|                 className="underline" | ||||
|                 href="https://ui.shadcn.com/docs/theming" | ||||
|               > | ||||
|                 shadcn theming docs | ||||
|               </a>{" "} | ||||
|               for usage. | ||||
|               Shows the 10 most recent events within the last 4 hours | ||||
|             </p> | ||||
| 
 | ||||
|             <div className="my-5"> | ||||
|               {colors.map((color, index) => ( | ||||
|                 <ColorSwatch | ||||
|                   key={index} | ||||
|                   name={color} | ||||
|                   value={`hsl(var(--${color}))`} | ||||
|                 /> | ||||
|               ))} | ||||
|             {!config && <ActivityIndicator />} | ||||
| 
 | ||||
|             {config && ( | ||||
|               <div> | ||||
|                 {events && events.length > 0 && ( | ||||
|                   <> | ||||
|                     <ActivityScrubber | ||||
|                       items={eventsToScrubberItems(events)} | ||||
|                       selectHandler={onSelect} | ||||
|                     /> | ||||
|                   </> | ||||
|                 )} | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             {config && ( | ||||
|               <div> | ||||
|                 {timeline && ( | ||||
|                   <> | ||||
|                     <TimelineScrubber eventID={timeline} /> | ||||
|                   </> | ||||
|                 )} | ||||
|               </div> | ||||
|             )} | ||||
| 
 | ||||
|             <div ref={contentRef}> | ||||
|               <Heading as="h4" className="my-5"> | ||||
|                 Timeline | ||||
|               </Heading> | ||||
|               <p className="text-small">Handlebar timestamp: {handlebarTime}</p> | ||||
|               <p className="text-small"> | ||||
|                 Handlebar is dragging: {isDragging ? "yes" : "no"} | ||||
|               </p> | ||||
|               <p> | ||||
|                 <Button onClick={handleZoomOut} disabled={zoomLevel === 0}> | ||||
|                   Zoom Out | ||||
|                 </Button> | ||||
|                 <Button | ||||
|                   onClick={handleZoomIn} | ||||
|                   disabled={zoomLevel === possibleZoomLevels.length - 1} | ||||
|                 > | ||||
|                   Zoom In | ||||
|                 </Button> | ||||
|               </p> | ||||
|               <Heading as="h4" className="my-5"> | ||||
|                 Color scheme | ||||
|               </Heading> | ||||
|               <p className="text-small"> | ||||
|                 Colors as set by the current theme. See the{" "} | ||||
|                 <a | ||||
|                   className="underline" | ||||
|                   href="https://ui.shadcn.com/docs/theming" | ||||
|                 > | ||||
|                   shadcn theming docs | ||||
|                 </a>{" "} | ||||
|                 for usage. | ||||
|               </p> | ||||
| 
 | ||||
|               <div className="my-5"> | ||||
|                 {colors.map((color, index) => ( | ||||
|                   <ColorSwatch | ||||
|                     key={index} | ||||
|                     name={color} | ||||
|                     value={`hsl(var(--${color}))`} | ||||
|                   /> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="flex-none"> | ||||
|           <EventReviewTimeline | ||||
|             segmentDuration={zoomSettings.segmentDuration} // seconds per segment
 | ||||
|             timestampSpread={zoomSettings.timestampSpread} // minutes between each major timestamp
 | ||||
|             timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
 | ||||
|             timelineEnd={Math.floor(Date.now() / 1000) - 6 * 60 * 60} // end of timeline - the later time
 | ||||
|             showHandlebar // show / hide the handlebar
 | ||||
|             handlebarTime={handlebarTime} // set the time of the handlebar
 | ||||
|             setHandlebarTime={setHandlebarTime} // expose handler to set the handlebar time
 | ||||
|             showMinimap // show / hide the minimap
 | ||||
|             minimapStartTime={Math.floor(Date.now() / 1000) - 35 * 60} // start time of the minimap - the earlier time (eg 1:00pm)
 | ||||
|             minimapEndTime={Math.floor(Date.now() / 1000) - 21 * 60} // end of the minimap - the later time (eg 3:00pm)
 | ||||
|             events={mockEvents} // events, including new has_been_reviewed and severity properties
 | ||||
|             severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
 | ||||
|             contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
 | ||||
|           /> | ||||
| 
 | ||||
|           <div className="w-[100px] overflow-y-auto no-scrollbar"> | ||||
|             <EventReviewTimeline | ||||
|               segmentDuration={zoomSettings.segmentDuration} // seconds per segment
 | ||||
|               timestampSpread={zoomSettings.timestampSpread} // minutes between each major timestamp
 | ||||
|               timelineStart={Math.floor(Date.now() / 1000)} // timestamp start of the timeline - the earlier time
 | ||||
|               timelineEnd={Math.floor(Date.now() / 1000) - 6 * 60 * 60} // end of timeline - the later time
 | ||||
|               showHandlebar // show / hide the handlebar
 | ||||
|               handlebarTime={handlebarTime} // set the time of the handlebar
 | ||||
|               setHandlebarTime={setHandlebarTime} // expose handler to set the handlebar time
 | ||||
|               onHandlebarDraggingChange={handleDraggingChange} // function for state of handlebar dragging
 | ||||
|               showMinimap // show / hide the minimap
 | ||||
|               minimapStartTime={minimapStartTime} // start time of the minimap - the earlier time (eg 1:00pm)
 | ||||
|               minimapEndTime={minimapEndTime} // end of the minimap - the later time (eg 3:00pm)
 | ||||
|               events={mockEvents} // events, including new has_been_reviewed and severity properties
 | ||||
|               severityType={"alert"} // choose the severity type for the middle line - all other severity types are to the right
 | ||||
|               contentRef={contentRef} // optional content ref where previews are, can be used for observing/scrolling later
 | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </> | ||||
|  | ||||
| @ -195,8 +195,8 @@ export default function DesktopEventView({ | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="relative w-full h-full"> | ||||
|       <div className="absolute flex justify-between left-0 top-0 right-0"> | ||||
|     <div className="flex flex-col w-full h-full"> | ||||
|       <div className="flex justify-between mb-2"> | ||||
|         <ToggleGroup | ||||
|           type="single" | ||||
|           defaultValue="alert" | ||||
| @ -261,55 +261,59 @@ export default function DesktopEventView({ | ||||
|         </Button> | ||||
|       )} | ||||
| 
 | ||||
|       <div | ||||
|         ref={contentRef} | ||||
|         className="absolute left-0 top-12 bottom-0 right-28 flex flex-wrap content-start gap-2 overflow-y-auto no-scrollbar" | ||||
|       > | ||||
|         {currentItems ? ( | ||||
|           currentItems.map((value, segIdx) => { | ||||
|             const lastRow = segIdx == reviewItems[severity].length - 1; | ||||
|             const relevantPreview = Object.values(relevantPreviews || []).find( | ||||
|               (preview) => | ||||
|                 preview.camera == value.camera && | ||||
|                 preview.start < value.start_time && | ||||
|                 preview.end > value.end_time | ||||
|             ); | ||||
|       <div className="flex h-full overflow-hidden"> | ||||
|         <div | ||||
|           ref={contentRef} | ||||
|           className="flex flex-1 flex-wrap content-start gap-2 overflow-y-auto no-scrollbar" | ||||
|         > | ||||
|           {currentItems ? ( | ||||
|             currentItems.map((value, segIdx) => { | ||||
|               const lastRow = segIdx == reviewItems[severity].length - 1; | ||||
|               const relevantPreview = Object.values( | ||||
|                 relevantPreviews || [] | ||||
|               ).find( | ||||
|                 (preview) => | ||||
|                   preview.camera == value.camera && | ||||
|                   preview.start < value.start_time && | ||||
|                   preview.end > value.end_time | ||||
|               ); | ||||
| 
 | ||||
|             return ( | ||||
|               <div | ||||
|                 key={value.id} | ||||
|                 ref={lastRow ? lastReviewRef : minimapRef} | ||||
|                 data-start={value.start_time} | ||||
|               > | ||||
|                 <div className="h-[234px] aspect-video rounded-lg overflow-hidden"> | ||||
|                   <PreviewThumbnailPlayer | ||||
|                     review={value} | ||||
|                     relevantPreview={relevantPreview} | ||||
|                     setReviewed={() => markItemAsReviewed(value.id)} | ||||
|                     onClick={() => onSelectReview(value.id)} | ||||
|                   /> | ||||
|               return ( | ||||
|                 <div | ||||
|                   key={value.id} | ||||
|                   ref={lastRow ? lastReviewRef : minimapRef} | ||||
|                   data-start={value.start_time} | ||||
|                 > | ||||
|                   <div className="h-[234px] aspect-video rounded-lg overflow-hidden"> | ||||
|                     <PreviewThumbnailPlayer | ||||
|                       review={value} | ||||
|                       relevantPreview={relevantPreview} | ||||
|                       setReviewed={() => markItemAsReviewed(value.id)} | ||||
|                       onClick={() => onSelectReview(value.id)} | ||||
|                     /> | ||||
|                   </div> | ||||
|                   {lastRow && !reachedEnd && <ActivityIndicator />} | ||||
|                 </div> | ||||
|                 {lastRow && !reachedEnd && <ActivityIndicator />} | ||||
|               </div> | ||||
|             ); | ||||
|           }) | ||||
|         ) : ( | ||||
|           <div ref={lastReviewRef} /> | ||||
|         )} | ||||
|       </div> | ||||
|       <div className="absolute top-12 right-0 bottom-0"> | ||||
|         <EventReviewTimeline | ||||
|           segmentDuration={60} | ||||
|           timestampSpread={15} | ||||
|           timelineStart={timeRange.before} | ||||
|           timelineEnd={timeRange.after} | ||||
|           showMinimap | ||||
|           minimapStartTime={minimapBounds.start} | ||||
|           minimapEndTime={minimapBounds.end} | ||||
|           events={reviewItems.all} | ||||
|           severityType={severity} | ||||
|           contentRef={contentRef} | ||||
|         /> | ||||
|               ); | ||||
|             }) | ||||
|           ) : ( | ||||
|             <div ref={lastReviewRef} /> | ||||
|           )} | ||||
|         </div> | ||||
|         <div className="md:w-[100px] overflow-y-auto no-scrollbar"> | ||||
|           <EventReviewTimeline | ||||
|             segmentDuration={60} | ||||
|             timestampSpread={15} | ||||
|             timelineStart={timeRange.before} | ||||
|             timelineEnd={timeRange.after} | ||||
|             showMinimap | ||||
|             minimapStartTime={minimapBounds.start} | ||||
|             minimapEndTime={minimapBounds.end} | ||||
|             events={reviewItems.all} | ||||
|             severityType={severity} | ||||
|             contentRef={contentRef} | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user