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:
@@ -9,10 +9,26 @@ import {
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../../../camera-manager/manager';
|
||||
import { ViewItemManager } from '../../../card-controller/view/item-manager';
|
||||
import { ViewManagerEpoch } from '../../../card-controller/view/types';
|
||||
import {
|
||||
MediaDetailsController,
|
||||
OverlayControlsContext,
|
||||
} from '../../../components-lib/media/details-controller';
|
||||
import { ThumbnailFeatureController } from '../../../components-lib/thumbnail/feature/controller';
|
||||
import { HomeAssistant } from '../../../ha/types';
|
||||
import { localize } from '../../../localize/localize';
|
||||
import thumbnailFeatureStyle from '../../../scss/thumbnail-feature.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../../../utils/action';
|
||||
import {
|
||||
downloadMedia,
|
||||
navigateToTimeline,
|
||||
toggleFavorite,
|
||||
toggleReviewed,
|
||||
} from '../../../utils/media-actions';
|
||||
import { dispatchShowOverlayMessageEvent } from '../../../utils/overlay-message';
|
||||
import { ViewItem } from '../../../view/item';
|
||||
import { ViewItemClassifier } from '../../../view/item-classifier';
|
||||
import '../../icon.js';
|
||||
import './thumbnail.js';
|
||||
|
||||
@@ -24,12 +40,33 @@ export class AdvancedCameraCardThumbnailFeature extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewManagerEpoch?: ViewManagerEpoch;
|
||||
|
||||
@property({ attribute: false })
|
||||
public item?: ViewItem;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hasDetails?: boolean;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_favorite_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_timeline_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_download_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_review_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_info_control = false;
|
||||
|
||||
private _controller = new ThumbnailFeatureController();
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
@@ -40,34 +77,158 @@ export class AdvancedCameraCardThumbnailFeature extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getControlContext(): OverlayControlsContext {
|
||||
return {
|
||||
hass: this.hass,
|
||||
viewItemManager: this.viewItemManager,
|
||||
viewManagerEpoch: this.viewManagerEpoch,
|
||||
capabilities: this.item ? this.viewItemManager?.getCapabilities(this.item) : null,
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const starClasses = {
|
||||
star: true,
|
||||
starred: ViewItemClassifier.isMedia(this.item) && !!this.item?.isFavorite(),
|
||||
};
|
||||
|
||||
const shouldShowTimelineControl =
|
||||
this.show_timeline_control && ViewItemClassifier.supportsTimeline(this.item);
|
||||
|
||||
const mediaCapabilities = this.viewItemManager?.getCapabilities(this.item) ?? null;
|
||||
|
||||
const shouldShowFavoriteControl =
|
||||
this.show_favorite_control &&
|
||||
this.item &&
|
||||
this.hass &&
|
||||
mediaCapabilities?.canFavorite;
|
||||
|
||||
const shouldShowDownloadControl =
|
||||
this.show_download_control &&
|
||||
this.hass &&
|
||||
this.item.getID() &&
|
||||
mediaCapabilities?.canDownload;
|
||||
|
||||
const isReviewed = ViewItemClassifier.isReview(this.item)
|
||||
? this.item.isReviewed()
|
||||
: null;
|
||||
const shouldShowReviewControl = this.show_review_control && isReviewed !== null;
|
||||
|
||||
const shouldShowInfoControl =
|
||||
this.show_info_control && ViewItemClassifier.isMedia(this.item);
|
||||
|
||||
const title = this._controller.getTitle();
|
||||
const subtitles = this._controller.getSubtitles();
|
||||
const iconClasses = classMap({
|
||||
background: title || subtitles.length,
|
||||
});
|
||||
const hasText = !!title || !!subtitles.length;
|
||||
|
||||
const mainIconClasses = {
|
||||
placeholder: true,
|
||||
};
|
||||
|
||||
const thumbnailClass = this._controller.getThumbnailClass();
|
||||
const thumbnailClasses = classMap({
|
||||
...(thumbnailClass && { [thumbnailClass]: true }),
|
||||
'has-text': hasText,
|
||||
});
|
||||
|
||||
return html`
|
||||
${this._controller.getThumbnail()
|
||||
? html` <advanced-camera-card-thumbnail-feature-thumbnail
|
||||
class="${thumbnailClasses}"
|
||||
.hass=${this.hass}
|
||||
.thumbnail=${this._controller.getThumbnail()}
|
||||
aria-label=${this.item?.getTitle() ?? ''}
|
||||
title=${this.item?.getTitle() ?? ''}
|
||||
></advanced-camera-card-thumbnail-feature-thumbnail>`
|
||||
: this._controller.getIcon()
|
||||
? html`<advanced-camera-card-icon
|
||||
class="${iconClasses}"
|
||||
.icon=${{ icon: this._controller.getIcon() }}
|
||||
<div class=${classMap({ media: true, 'has-text': hasText })}>
|
||||
${this._controller.getThumbnail()
|
||||
? html` <advanced-camera-card-thumbnail-feature-thumbnail
|
||||
class="${thumbnailClasses}"
|
||||
.hass=${this.hass}
|
||||
.thumbnail=${this._controller.getThumbnail()}
|
||||
aria-label=${this.item?.getTitle() ?? ''}
|
||||
title=${this.item?.getTitle() ?? ''}
|
||||
></advanced-camera-card-thumbnail-feature-thumbnail>`
|
||||
: this._controller.getIcon()
|
||||
? html`<advanced-camera-card-icon
|
||||
class=${classMap(mainIconClasses)}
|
||||
.icon=${{ icon: this._controller.getIcon() }}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
</div>
|
||||
${shouldShowReviewControl
|
||||
? html`<advanced-camera-card-icon
|
||||
class="review"
|
||||
title=${isReviewed
|
||||
? localize('common.set_reviews.unreviewed')
|
||||
: localize('common.set_reviews.reviewed')}
|
||||
.icon=${{
|
||||
icon: isReviewed ? 'mdi:check-circle' : 'mdi:check-circle-outline',
|
||||
}}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.item) {
|
||||
await toggleReviewed(this.item, this._getControlContext());
|
||||
}
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: shouldShowFavoriteControl
|
||||
? html` <advanced-camera-card-icon
|
||||
class="${classMap(starClasses)}"
|
||||
title=${localize('thumbnail.retain_indefinitely')}
|
||||
.icon=${{
|
||||
icon: this.item.isFavorite() ? 'mdi:star' : 'mdi:star-outline',
|
||||
}}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (
|
||||
this.item &&
|
||||
(await toggleFavorite(this.item, this._getControlContext()))
|
||||
) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
${title || subtitles.length
|
||||
: ``}
|
||||
${shouldShowInfoControl
|
||||
? html`<advanced-camera-card-icon
|
||||
class="info"
|
||||
.icon=${{ icon: 'mdi:information-outline' }}
|
||||
title=${this.item?.getDescription() ?? ''}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
const detailsController = new MediaDetailsController();
|
||||
detailsController.calculate(this.cameraManager, this.item);
|
||||
dispatchShowOverlayMessageEvent(
|
||||
this,
|
||||
detailsController.getMessage(this._getControlContext()),
|
||||
);
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
${shouldShowTimelineControl
|
||||
? html`<advanced-camera-card-icon
|
||||
class="timeline"
|
||||
.icon=${{ icon: 'mdi:target' }}
|
||||
title=${localize('thumbnail.timeline')}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.item) {
|
||||
navigateToTimeline(this.item, this._getControlContext());
|
||||
}
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
${shouldShowDownloadControl
|
||||
? html` <advanced-camera-card-icon
|
||||
class="download"
|
||||
.icon=${{ icon: 'mdi:download' }}
|
||||
title=${localize('thumbnail.download')}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.item) {
|
||||
await downloadMedia(this.item, this._getControlContext());
|
||||
}
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ``}
|
||||
${hasText
|
||||
? html`
|
||||
${title ? html`<div class="title">${title}</div>` : ''}
|
||||
${subtitles.length
|
||||
|
||||
Reference in New Issue
Block a user