Files
advanced-camera-card/src/components/thumbnail/feature/feature.ts
T
Dermot Duffy fc32727860 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`.
2026-01-19 13:32:50 -08:00

256 lines
8.5 KiB
TypeScript

import {
CSSResult,
LitElement,
PropertyValues,
TemplateResult,
html,
unsafeCSS,
} from 'lit';
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';
@customElement('advanced-camera-card-thumbnail-feature')
export class AdvancedCameraCardThumbnailFeature extends LitElement {
@property({ attribute: false })
public hass?: HomeAssistant;
@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 {
if (
['item', 'hasDetails', 'cameraManager'].some((prop) => changedProperties.has(prop))
) {
this._controller.calculate(this.cameraManager, this.item, this.hasDetails);
}
}
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 hasText = !!title || !!subtitles.length;
const mainIconClasses = {
placeholder: true,
};
const thumbnailClass = this._controller.getThumbnailClass();
const thumbnailClasses = classMap({
...(thumbnailClass && { [thumbnailClass]: true }),
'has-text': hasText,
});
return html`
<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>`
: ``}
${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
? html`<div>
${subtitles.map(
(subtitle) => html`<div class="subtitle">${subtitle}</div>`,
)}
</div>`
: ''}
`
: html``}
`;
}
static get styles(): CSSResult {
return unsafeCSS(thumbnailFeatureStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'advanced-camera-card-thumbnail-feature': AdvancedCameraCardThumbnailFeature;
}
}