mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add recordings timeline entry for frigate+ attributes (#8063)
* Add attribute item to timeline * Add face icon * Add support for other icons * Cleanup * Ensure attributes are only updated once * don't show _ in attributes
This commit is contained in:
		
							parent
							
								
									79fabbb6b0
								
							
						
					
					
						commit
						08ef69bac4
					
				@ -42,6 +42,9 @@ def should_update_state(prev_event: Event, current_event: Event) -> bool:
 | 
			
		||||
    if prev_event["stationary"] != current_event["stationary"]:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    if prev_event["attributes"] != current_event["attributes"]:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -74,6 +74,7 @@ class TimelineProcessor(threading.Thread):
 | 
			
		||||
                    camera_config.detect.height,
 | 
			
		||||
                    event_data["region"],
 | 
			
		||||
                ),
 | 
			
		||||
                "attribute": "",
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        if event_type == "start":
 | 
			
		||||
@ -93,6 +94,12 @@ class TimelineProcessor(threading.Thread):
 | 
			
		||||
                    "stationary" if event_data["stationary"] else "active"
 | 
			
		||||
                )
 | 
			
		||||
                Timeline.insert(timeline_entry).execute()
 | 
			
		||||
            elif prev_event_data["attributes"] == {} and event_data["attributes"] != {}:
 | 
			
		||||
                timeline_entry[Timeline.class_type] = "attribute"
 | 
			
		||||
                timeline_entry[Timeline.data]["attribute"] = list(
 | 
			
		||||
                    event_data["attributes"].keys()
 | 
			
		||||
                )[0]
 | 
			
		||||
                Timeline.insert(timeline_entry).execute()
 | 
			
		||||
        elif event_type == "end":
 | 
			
		||||
            timeline_entry[Timeline.class_type] = "gone"
 | 
			
		||||
            Timeline.insert(timeline_entry).execute()
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,10 @@ import ActiveObjectIcon from '../icons/ActiveObject';
 | 
			
		||||
import PlayIcon from '../icons/Play';
 | 
			
		||||
import ExitIcon from '../icons/Exit';
 | 
			
		||||
import StationaryObjectIcon from '../icons/StationaryObject';
 | 
			
		||||
import { Zone } from '../icons/Zone';
 | 
			
		||||
import FaceIcon from '../icons/Face';
 | 
			
		||||
import LicensePlateIcon from '../icons/LicensePlate';
 | 
			
		||||
import DeliveryTruckIcon from '../icons/DeliveryTruck';
 | 
			
		||||
import ZoneIcon from '../icons/Zone';
 | 
			
		||||
import { useMemo, useState } from 'preact/hooks';
 | 
			
		||||
import Button from './Button';
 | 
			
		||||
 | 
			
		||||
@ -88,7 +91,7 @@ export default function TimelineSummary({ event, onFrameSelected }) {
 | 
			
		||||
              aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
 | 
			
		||||
              onClick={() => onSelectMoment(index)}
 | 
			
		||||
            >
 | 
			
		||||
              {getTimelineIcon(item.class_type)}
 | 
			
		||||
              {getTimelineIcon(item)}
 | 
			
		||||
            </Button>
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
@ -113,8 +116,8 @@ export default function TimelineSummary({ event, onFrameSelected }) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTimelineIcon(classType) {
 | 
			
		||||
  switch (classType) {
 | 
			
		||||
function getTimelineIcon(timelineItem) {
 | 
			
		||||
  switch (timelineItem.class_type) {
 | 
			
		||||
    case 'visible':
 | 
			
		||||
      return <PlayIcon className="w-8" />;
 | 
			
		||||
    case 'gone':
 | 
			
		||||
@ -124,7 +127,16 @@ function getTimelineIcon(classType) {
 | 
			
		||||
    case 'stationary':
 | 
			
		||||
      return <StationaryObjectIcon className="w-8" />;
 | 
			
		||||
    case 'entered_zone':
 | 
			
		||||
      return <Zone className="w-8" />;
 | 
			
		||||
      return <ZoneIcon className="w-8" />;
 | 
			
		||||
    case 'attribute':
 | 
			
		||||
      switch (timelineItem.data.attribute) {
 | 
			
		||||
        case 'face':
 | 
			
		||||
          return <FaceIcon className="w-8" />;
 | 
			
		||||
        case 'license_plate':
 | 
			
		||||
          return <LicensePlateIcon className="w-8" />;
 | 
			
		||||
        default:
 | 
			
		||||
          return <DeliveryTruckIcon className="w-8" />;
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -156,6 +168,15 @@ function getTimelineItemDescription(config, timelineItem, event) {
 | 
			
		||||
        time_style: 'medium',
 | 
			
		||||
        time_format: config.ui.time_format,
 | 
			
		||||
      })}`;
 | 
			
		||||
    case 'attribute':
 | 
			
		||||
      return `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label} at ${formatUnixTimestampToDateTime(
 | 
			
		||||
        timelineItem.timestamp,
 | 
			
		||||
        {
 | 
			
		||||
          date_style: 'short',
 | 
			
		||||
          time_style: 'medium',
 | 
			
		||||
          time_format: config.ui.time_format,
 | 
			
		||||
        }
 | 
			
		||||
      )}`;
 | 
			
		||||
    case 'gone':
 | 
			
		||||
      return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
 | 
			
		||||
        date_style: 'short',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								web/src/icons/DeliveryTruck.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								web/src/icons/DeliveryTruck.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import { h } from 'preact';
 | 
			
		||||
import { memo } from 'preact/compat';
 | 
			
		||||
 | 
			
		||||
export function StationaryObject({ className = '' }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg className={`fill-current ${className}`} viewBox="0 -960 960 960">
 | 
			
		||||
      <path
 | 
			
		||||
        fill="currentColor"
 | 
			
		||||
        d="M240-160q-50 0-85-35t-35-85H40v-440q0-33 23.5-56.5T120-800h560v160h120l120 160v200h-80q0 50-35 85t-85 35q-50 0-85-35t-35-85H360q0 50-35 85t-85 35Zm0-80q17 0 28.5-11.5T280-280q0-17-11.5-28.5T240-320q-17 0-28.5 11.5T200-280q0 17 11.5 28.5T240-240ZM120-360h32q17-18 39-29t49-11q27 0 49 11t39 29h272v-360H120v360Zm600 120q17 0 28.5-11.5T760-280q0-17-11.5-28.5T720-320q-17 0-28.5 11.5T680-280q0 17 11.5 28.5T720-240Zm-40-200h170l-90-120h-80v120ZM360-540Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default memo(StationaryObject);
 | 
			
		||||
							
								
								
									
										22
									
								
								web/src/icons/Face.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								web/src/icons/Face.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import { h } from 'preact';
 | 
			
		||||
import { memo } from 'preact/compat';
 | 
			
		||||
 | 
			
		||||
export function Zone({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      className={className}
 | 
			
		||||
      fill={fill}
 | 
			
		||||
      viewBox="0 -960 960 960"
 | 
			
		||||
      stroke={stroke}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill="currentColor"
 | 
			
		||||
        d="M360-390q-21 0-35.5-14.5T310-440q0-21 14.5-35.5T360-490q21 0 35.5 14.5T410-440q0 21-14.5 35.5T360-390Zm240 0q-21 0-35.5-14.5T550-440q0-21 14.5-35.5T600-490q21 0 35.5 14.5T650-440q0 21-14.5 35.5T600-390ZM480-160q134 0 227-93t93-227q0-24-3-46.5T786-570q-21 5-42 7.5t-44 2.5q-91 0-172-39T390-708q-32 78-91.5 135.5T160-486v6q0 134 93 227t227 93Zm0 80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-54-715q42 70 114 112.5T700-640q14 0 27-1.5t27-3.5q-42-70-114-112.5T480-800q-14 0-27 1.5t-27 3.5ZM177-581q51-29 89-75t57-103q-51 29-89 75t-57 103Zm249-214Zm-103 36Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default memo(Zone);
 | 
			
		||||
							
								
								
									
										15
									
								
								web/src/icons/LicensePlate.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								web/src/icons/LicensePlate.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import { h } from 'preact';
 | 
			
		||||
import { memo } from 'preact/compat';
 | 
			
		||||
 | 
			
		||||
export function StationaryObject({ className = '' }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg className={`fill-current ${className}`} viewBox="0 -960 960 960">
 | 
			
		||||
      <path
 | 
			
		||||
        fill="currentColor"
 | 
			
		||||
        d="M400-280h360v-240H400v240ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default memo(StationaryObject);
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user