feat: Add support for Frigate reviews / detections [initial PR] (#2315)

- Add support for Frigate reviews / detections.
 - Add support for GenAI metadata.
- Significant internal refactor to more flexible "UnifiedQuery" to allow
mixing cameras with simple metadata and review metadata (e.g. a timeline
view of a Frigate camera with reviews, and a Reolink camera with simple
metadata).
 - Add support for folder media as camera media.

There are a few more PRs to commit prior to this going live, but
commiting this for now due to the scale of the change.

BREAKING CHANGE: `media_type` and `events_type` are retired under
`live`, `viewer` and `timeline` configuration sections, instead media
type is associated (once) with the camera under `media`.
This commit is contained in:
Dermot Duffy
2026-01-19 13:32:50 -08:00
committed by GitHub
parent 6eb0a87ca8
commit fc32727860
215 changed files with 14491 additions and 6160 deletions
+280
View File
@@ -0,0 +1,280 @@
@use './z-index.scss' as *;
:host {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: flex-end;
padding: 24px;
pointer-events: none;
z-index: $z-index-overlay-message;
}
.backdrop {
position: absolute;
inset: 0;
pointer-events: auto;
}
.message {
position: relative;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 12px;
width: min(92%, 400px);
max-height: calc(100% - 48px);
padding: 20px 24px;
padding-right: 48px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--advanced-camera-card-overlay-message-scrollbar-color)
transparent;
// Glassmorphism background
background: var(--advanced-camera-card-overlay-message-background);
backdrop-filter: var(--advanced-camera-card-overlay-message-backdrop-filter);
-webkit-backdrop-filter: var(--advanced-camera-card-overlay-message-backdrop-filter);
// Borders & Edge highlights
border: var(--advanced-camera-card-overlay-message-border);
border-top: var(--advanced-camera-card-overlay-message-border-top);
border-radius: var(--advanced-camera-card-border-radius-final);
// Shadows
box-shadow: var(
--advanced-camera-card-box-shadow-override,
var(--advanced-camera-card-overlay-message-box-shadow)
);
pointer-events: auto;
// Entry animation (auto-plays on render)
animation: slideUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
// Exit animation
.message.exiting {
animation: slideDown 0.25s ease-out forwards;
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
60% {
opacity: 1;
transform: translateY(-6px) scale(1.02);
}
80% {
transform: translateY(3px) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes slideDown {
0% {
opacity: 1;
transform: translateY(0) scale(1);
}
20% {
transform: translateY(-4px) scale(1.02);
}
100% {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
}
.icon {
flex-shrink: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
color: var(--advanced-camera-card-overlay-message-icon-color);
filter: drop-shadow(
0 0 8px
color-mix(
in oklab,
var(--advanced-camera-card-overlay-message-icon-color),
transparent 60%
)
);
}
.icon advanced-camera-card-icon {
--mdc-icon-size: 20px;
}
.text {
flex: 1;
font-size: 15px;
font-weight: 400;
line-height: 1.5;
color: var(--advanced-camera-card-overlay-message-text-color);
word-break: break-word;
}
.controls {
position: absolute;
top: 8px;
right: 48px;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: var(--advanced-camera-card-button-border-radius);
background: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
transparent 92%
);
}
.control {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--advanced-camera-card-button-border-radius);
color: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
transparent 40%
);
cursor: pointer;
transition: all 0.15s ease;
advanced-camera-card-icon {
--mdc-icon-size: 16px;
}
&:hover {
filter: brightness(1.2);
}
// Base control hover color transition (from dimmed to full).
&:not([class^='emphasis-']):hover {
color: var(--advanced-camera-card-overlay-message-close-color);
}
&.emphasis-low {
color: var(--advanced-camera-card-foreground-severity-low-color);
}
&.emphasis-medium {
color: var(--advanced-camera-card-foreground-severity-medium-color);
}
&.emphasis-high {
color: var(--advanced-camera-card-foreground-severity-high-color);
}
}
.close {
position: absolute;
top: 8px;
right: 8px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--advanced-camera-card-button-border-radius);
background: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
transparent 95%
);
color: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
transparent 40%
);
cursor: pointer;
transition: all 0.15s ease;
}
.close:hover {
background: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
transparent 90%
);
color: var(--advanced-camera-card-overlay-message-close-color);
}
.close advanced-camera-card-icon {
--mdc-icon-size: 18px;
}
.details {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px 16px;
flex: 1;
min-width: 0;
}
.detail {
display: flex;
align-items: center;
gap: 8px;
color: var(--advanced-camera-card-overlay-message-text-color);
font-size: 14px;
line-height: 1.4;
advanced-camera-card-icon {
flex-shrink: 0;
--mdc-icon-size: 18px;
opacity: 0.7;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
&.heading {
width: 100%;
font-weight: 600;
font-size: 16px;
margin-bottom: 4px;
advanced-camera-card-icon {
opacity: 1;
}
}
}
.description {
width: 100%;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid
color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-text-color),
transparent 80%
);
color: var(--advanced-camera-card-overlay-message-text-color);
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
+14
View File
@@ -82,3 +82,17 @@ advanced-camera-card-icon.item {
// Further theme related styling is dynamically applied by `status-bar.ts`, see
// `_renderPerInstanceStyle`.
/*****************
* Severity colors
*****************/
.item[data-severity='high'] {
color: var(--advanced-camera-card-foreground-severity-high-color);
}
.item[data-severity='medium'] {
color: var(--advanced-camera-card-foreground-severity-medium-color);
}
.item[data-severity='low'] {
color: var(--advanced-camera-card-foreground-severity-low-color);
}
+57 -1
View File
@@ -38,6 +38,16 @@
);
--advanced-camera-card-control-foreground: var(--app-header-text-color);
--advanced-camera-card-foreground-severity-high-color: var(
--ha-color-fill-danger-loud-resting
);
--advanced-camera-card-foreground-severity-medium-color: var(
--ha-color-fill-warning-loud-resting
);
--advanced-camera-card-foreground-severity-low-color: var(
--ha-color-fill-neutral-loud-resting
);
/*********
* Buttons
*********/
@@ -51,7 +61,9 @@
);
// The border radius of buttons.
--advanced-camera-card-button-border-radius: 50%;
// We use a large viewport-relative value to ensure that rectangular
// elements render as pills/capsules rather than ovals.
--advanced-camera-card-button-border-radius: 100vmax;
/******
* Menu
@@ -126,6 +138,16 @@
--advanced-camera-card-thumbnail-icon-color: var(
--advanced-camera-card-foreground-primary
);
--advanced-camera-card-thumbnail-icon-severity-high-color: var(
--advanced-camera-card-foreground-severity-high-color
);
--advanced-camera-card-thumbnail-icon-severity-medium-color: var(
--advanced-camera-card-foreground-severity-medium-color
);
--advanced-camera-card-thumbnail-icon-severity-low-color: var(
--advanced-camera-card-foreground-severity-low-color
);
--advanced-camera-card-thumbnail-border-color: var(
--advanced-camera-card-foreground-primary
);
@@ -135,6 +157,9 @@
gold,
transparent 25%
);
--advanced-camera-card-thumbnail-icon-review-color: var(
--advanced-camera-card-thumbnail-icon-color
);
--advanced-camera-card-thumbnail-text-color: var(--advanced-camera-card-text-color);
/**********
@@ -164,6 +189,16 @@
--advanced-camera-card-active-color
);
--advanced-camera-card-timeline-item-severity-high-color: var(
--advanced-camera-card-foreground-severity-high-color
);
--advanced-camera-card-timeline-item-severity-medium-color: var(
--advanced-camera-card-foreground-severity-medium-color
);
--advanced-camera-card-timeline-item-severity-low-color: var(
--advanced-camera-card-foreground-severity-low-color
);
/*******************
* Loading Indicator
*******************/
@@ -227,4 +262,25 @@
var(--advanced-camera-card-ptz-background),
transparent var(--advanced-camera-card-control-background-opacity)
);
/******************
* Overlay Message
******************/
--advanced-camera-card-overlay-message-background: linear-gradient(
135deg,
rgba(35, 35, 35, 0.75) 0%,
rgba(15, 15, 15, 0.65) 100%
);
--advanced-camera-card-overlay-message-backdrop-filter: blur(20px) saturate(180%);
--advanced-camera-card-overlay-message-border: 1px solid rgba(255, 255, 255, 0.1);
--advanced-camera-card-overlay-message-border-top: 1px solid rgba(255, 255, 255, 0.15);
--advanced-camera-card-overlay-message-box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5),
0 2px 8px 0 rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
--advanced-camera-card-overlay-message-text-color: #ffffff;
--advanced-camera-card-overlay-message-icon-color: var(
--advanced-camera-card-foreground-primary
);
--advanced-camera-card-overlay-message-scrollbar-color: rgba(255, 255, 255, 0.2);
--advanced-camera-card-overlay-message-close-color: #ffffff;
}
+16
View File
@@ -15,4 +15,20 @@
// See: https://github.com/home-assistant/frontend/blob/dev/src/common/dom/apply_themes_on_element.ts
--app-header-background-color: #03a9f4;
/******************
* Overlay Message
******************/
--advanced-camera-card-overlay-message-background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.85) 0%,
rgba(235, 235, 235, 0.75) 100%
);
--advanced-camera-card-overlay-message-text-color: var(--primary-text-color);
--advanced-camera-card-overlay-message-border: 1px solid rgba(0, 0, 0, 0.3);
--advanced-camera-card-overlay-message-border-top: 1px solid rgba(0, 0, 0, 0.2);
--advanced-camera-card-overlay-message-box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3),
0 2px 8px 0 rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.8);
--advanced-camera-card-overlay-message-scrollbar-color: rgba(0, 0, 0, 0.5);
--advanced-camera-card-overlay-message-close-color: #000000;
}
+41 -21
View File
@@ -2,37 +2,57 @@
display: flex;
flex-direction: column;
width: 100%;
margin-left: 5px;
padding: 5px;
color: var(--advanced-camera-card-thumbnail-text-color);
overflow: hidden;
column-gap: 5%;
}
// Wrap "details that don't fit" off the side instead of showing part of a
// detail.
align-content: flex-start;
flex-wrap: wrap;
div.heading {
font-size: 1.2rem;
font-weight: bold;
}
div.details {
flex: 1;
justify-content: center;
display: flex;
flex-direction: column;
flex-wrap: wrap;
--mdc-icon-size: 16px;
width: 100%;
// Details panel can shrink as well as grow.
min-height: 0px;
max-height: var(--advanced-camera-card-thumbnail-size);
margin-left: 5px;
color: var(--advanced-camera-card-thumbnail-text-color);
overflow: hidden;
--mdc-icon-size: 16px;
}
div.details div {
div {
width: 100%;
display: flex;
align-items: center;
gap: 5px;
container-type: normal;
font-size: 0.95cqh;
&.heading {
font-weight: bold;
font-size: 1cqh;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
}
div.details div * {
margin: 0px 3px;
[severity='high'] {
color: var(--advanced-camera-card-thumbnail-icon-severity-high-color);
}
[severity='medium'] {
color: var(--advanced-camera-card-thumbnail-icon-severity-medium-color);
}
[severity='low'] {
color: var(--advanced-camera-card-thumbnail-icon-severity-low-color);
}
+1 -2
View File
@@ -1,10 +1,9 @@
:host {
display: block;
display: flex;
overflow: hidden;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
}
+76 -12
View File
@@ -15,24 +15,41 @@
box-sizing: border-box;
// Background icon relative to the thumbnail.
overflow: hidden;
// Relative for control icons.
position: relative;
}
advanced-camera-card-thumbnail-feature-thumbnail {
height: 50%;
.media {
// Relative for thumbnail.
position: relative;
width: 100%;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
min-height: 0;
overflow: hidden;
}
advanced-camera-card-thumbnail-feature-thumbnail.brand:only-child {
height: 60%;
.media.has-text {
flex: 0 0 50%;
}
advanced-camera-card-thumbnail-feature-thumbnail:not(.brand):only-child {
// Regular camera thumbnails: fill the media container
advanced-camera-card-thumbnail-feature-thumbnail:not(.placeholder) {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
advanced-camera-card-icon:only-child {
width: 60%;
height: 60%;
// Placeholder icons/thumbnails: centered, 90% of container height
advanced-camera-card-thumbnail-feature-thumbnail.placeholder,
advanced-camera-card-icon.placeholder {
height: 90%;
aspect-ratio: 1 / 1;
--mdc-icon-size: 100%;
}
@@ -57,10 +74,57 @@ advanced-camera-card-icon.background {
div {
text-align: center;
color: var(--primary-text-color);
z-index: 1;
}
div.title {
font-size: 1.5rem;
font-size: 1rem;
line-height: 1.2;
}
div.subtitle {
font-size: 0.8em;
font-size: 0.75rem;
line-height: 1.2;
}
advanced-camera-card-icon:not(.placeholder):not(.background) {
position: absolute;
border-radius: 50%;
color: var(--advanced-camera-card-thumbnail-icon-color);
z-index: 2;
cursor: pointer;
transition:
opacity 0.2s ease-in-out,
color 0.4s ease-in-out;
}
advanced-camera-card-icon:not(.placeholder):not(.background):hover {
opacity: 1;
color: var(--advanced-camera-card-active-color);
}
advanced-camera-card-icon.star,
advanced-camera-card-icon.review {
top: 2px;
left: 2px;
}
advanced-camera-card-icon.star.starred {
color: var(--advanced-camera-card-thumbnail-icon-favorite-color);
}
advanced-camera-card-icon.timeline {
left: 2px;
bottom: 2px;
}
advanced-camera-card-icon.info {
right: 2px;
top: 2px;
cursor: default;
}
advanced-camera-card-icon.download {
right: 2px;
bottom: 2px;
}
+4 -33
View File
@@ -2,6 +2,7 @@
display: flex;
flex-direction: row;
box-sizing: border-box;
gap: 2px;
// Ensure control icons are relative to the thumbnail.
position: relative;
@@ -19,6 +20,9 @@
border-radius: var(--advanced-camera-card-border-radius-final);
padding: 2px;
// Additional padding on the right to match the gap specified above.
padding-right: 4px;
// When details are enabled, use a background color so that the details have
// contrast with the background.
background-color: var(--advanced-camera-card-thumbnail-background);
@@ -28,39 +32,6 @@
transform: scale(1.04);
}
advanced-camera-card-icon {
position: absolute;
border-radius: 50%;
color: var(--advanced-camera-card-thumbnail-icon-color);
cursor: pointer;
transition:
opacity 0.2s ease-in-out,
color 0.2s ease-in-out;
}
advanced-camera-card-icon:hover {
opacity: 1;
}
advanced-camera-card-icon.star {
top: 3px;
left: 3px;
}
advanced-camera-card-icon.star.starred {
color: var(--advanced-camera-card-thumbnail-icon-favorite-color);
}
advanced-camera-card-icon.timeline {
top: 3px;
right: 3px;
}
advanced-camera-card-icon.download {
right: 3px;
bottom: 3px;
}
advanced-camera-card-thumbnail-details {
flex: 1;
}
+12
View File
@@ -65,6 +65,18 @@ div.timeline {
border-color ease-in-out 1s,
box-shadow ease-in-out 1s;
}
.vis-item[data-severity='high'] {
border-color: var(--advanced-camera-card-timeline-item-severity-high-color);
background-color: var(--advanced-camera-card-timeline-item-severity-high-color);
}
.vis-item[data-severity='medium'] {
border-color: var(--advanced-camera-card-timeline-item-severity-medium-color);
background-color: var(--advanced-camera-card-timeline-item-severity-medium-color);
}
.vis-item[data-severity='low'] {
border-color: var(--advanced-camera-card-timeline-item-severity-low-color);
background-color: var(--advanced-camera-card-timeline-item-severity-low-color);
}
.vis-item.vis-selected {
border-color: var(--advanced-camera-card-active-color);
background-color: var(--advanced-camera-card-active-color);
+1
View File
@@ -16,5 +16,6 @@ $z-index-submenu: 5;
// Need menu to render above drawer (so the drawer button is below menu-submenus)
$z-index-menu: 4;
$z-index-overlay-message: 4;
$z-index-drawer: 3;
$z-index-status-bar: 2;