diff --git a/src/components/gallery.ts b/src/components/gallery.ts index 28b6cc2c..d0391272 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { HomeAssistant } from 'custom-card-helpers'; import { css, CSSResultGroup, @@ -13,6 +12,7 @@ import { customElement, property } from 'lit/decorators.js'; import galleryStyle from '../scss/gallery.scss'; import { CameraConfig, + ExtendedHomeAssistant, frigateCardConfigDefaults, GalleryConfig, THUMBNAIL_WIDTH_MAX, @@ -31,7 +31,7 @@ import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js'; @customElement('frigate-card-gallery') export class FrigateCardGallery extends LitElement { @property({ attribute: false }) - public hass?: HomeAssistant; + public hass?: ExtendedHomeAssistant; @property({ attribute: false }) public view?: Readonly; @@ -109,7 +109,7 @@ export class FrigateCardGallery extends LitElement { @customElement('frigate-card-gallery-core') export class FrigateCardGalleryCore extends LitElement { @property({ attribute: false }) - public hass?: HomeAssistant; + public hass?: ExtendedHomeAssistant; @property({ attribute: false }) public view?: Readonly; diff --git a/src/components/live.ts b/src/components/live.ts index 6ab3af26..9f29bf85 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -138,6 +138,19 @@ export class FrigateCardLive extends LitElement { } } + /** + * Determine whether the element should be updated. + * @param _changedProps The changed properties if any. + * @returns `true` if the element should be updated. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected shouldUpdate(_changedProps: PropertyValues): boolean { + // Don't process updates if it's in the background and a message was + // received (otherwise an error message thrown by the background live + // component may continually be re-spammed hitting performance). + return !this._inBackground || !this._messageReceivedPostRender; + } + /** * Component connected callback. */ diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts index b218304d..897d081c 100644 --- a/src/components/surround-thumbnails.ts +++ b/src/components/surround-thumbnails.ts @@ -1,4 +1,3 @@ -import { HomeAssistant } from 'custom-card-helpers'; import { CSSResultGroup, html, @@ -12,6 +11,7 @@ import surroundThumbnailsStyle from '../scss/surround.scss'; import { BrowseMediaQueryParameters, CameraConfig, + ExtendedHomeAssistant, FrigateBrowseMediaSource, FrigateCardError, FrigateCardView, @@ -30,7 +30,7 @@ import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; @customElement('frigate-card-surround-thumbnails') export class FrigateCardSurround extends LitElement { @property({ attribute: false }) - public hass?: HomeAssistant; + public hass?: ExtendedHomeAssistant; @property({ attribute: false }) public view?: Readonly; diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index c5003787..9136a54d 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -1,4 +1,3 @@ -import { HomeAssistant } from 'custom-card-helpers'; import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures'; import { @@ -15,6 +14,7 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js'; import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss'; import { CameraConfig, + ExtendedHomeAssistant, FrigateBrowseMediaSource, ThumbnailsControlConfig, } from '../types.js'; @@ -36,7 +36,7 @@ export interface ThumbnailCarouselTap { @customElement('frigate-card-thumbnail-carousel') export class FrigateCardThumbnailCarousel extends LitElement { @property({ attribute: false }) - public hass?: HomeAssistant; + public hass?: ExtendedHomeAssistant; @property({ attribute: false }) public view?: Readonly; diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index d365420f..bc6e829a 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -1,4 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; +import { Task } from '@lit-labs/task/task.js'; import { format, fromUnixTime } from 'date-fns'; import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @@ -9,6 +9,7 @@ import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss'; import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording.scss'; import thumbnailStyle from '../scss/thumbnail.scss'; import type { + ExtendedHomeAssistant, FrigateBrowseMediaSource, FrigateEvent, FrigateRecording, @@ -17,7 +18,9 @@ import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { errorToConsole, prettifyTitle } from '../utils/basic.js'; import { retainEvent } from '../utils/frigate.js'; import { getEventDurationString } from '../utils/ha/browse-media.js'; +import { homeAssistantSignPath } from '../utils/ha/index.js'; import { View } from '../view.js'; +import { dispatchFrigateCardErrorEvent, renderProgressIndicator } from './message.js'; // The minimum width of a thumbnail with details enabled. export const THUMBNAIL_DETAILS_WIDTH_MIN = 300; @@ -27,10 +30,51 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement { @property({ attribute: false }) public thumbnail?: string; + @property({ attribute: false }) + public hass?: ExtendedHomeAssistant; + + protected _signThumbnailTask = new Task( + this, + this._signThumbnail.bind(this), + // Do not re-run the task if hass changes, unless it was previously undefined. + (): [boolean, string | undefined] => [!!this.hass, this.thumbnail], + ); + + public count = 0; + + /** + * Sign a thumbnail URL if necessary. May throw. + * @param param0 A list of lit-task dependencies. + * @returns A signed URL or null. + */ + protected async _signThumbnail([haveHASS, thumbnail]: [ + boolean, + string | undefined, + ]): Promise { + this.count++; + if (!haveHASS || !this.hass || !thumbnail) { + return null; + } + if (this.thumbnail?.startsWith('data:')) { + return this.thumbnail; + } + return await homeAssistantSignPath(this.hass, thumbnail); + } + protected render(): TemplateResult | void { return html` ${this.thumbnail - ? html`` + ? html` ${this._signThumbnailTask.render({ + initial: () => renderProgressIndicator(), + pending: () => renderProgressIndicator(), + error: (e: unknown) => { + errorToConsole(e as Error); + dispatchFrigateCardErrorEvent(this, e as Error); + }, + complete: (signedThumbnail: string | null) => { + return signedThumbnail ? html`` : html``; + }, + })}` : html`; - + // ====================== + // Target-based interface + // ====================== @property({ attribute: false }) public target?: FrigateBrowseMediaSource | null; @property({ attribute: false }) public childIndex?: number; - // ============================================================= - // Overrides that can be used if data bindings are not available - // ============================================================= + // =================================================== + // Raw interface (can override target-based interface) + // =================================================== @property({ attribute: true }) public thumbnail?: string; @property({ attribute: true }) public label?: string; - @property({ attribute: true }) - public event?: string; + @property({ attribute: false }) + public event?: FrigateEvent; // ================================ // Optional parameters for controls // ================================ @property({ attribute: false }) - public hass?: HomeAssistant; + public view?: Readonly; + + @property({ attribute: false }) + public hass?: ExtendedHomeAssistant; @property({ attribute: false }) public clientID?: string; @@ -188,7 +232,7 @@ export class FrigateCardThumbnail extends LitElement { // Always give the overrides preference (if specified). if (this.event) { - event = JSON.parse(this.event); + event = this.event; } thumbnail = this.thumbnail ? this.thumbnail : thumbnail; label = this.label ? this.label : label; @@ -206,6 +250,7 @@ export class FrigateCardThumbnail extends LitElement { ? html`` diff --git a/src/components/timeline.ts b/src/components/timeline.ts index 7fa68f7d..de70feb1 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -96,7 +96,10 @@ interface CameraRecordings { summary: RecordingSummary; } -const isHoverableDevice = window.matchMedia('(hover: hover) and (pointer: fine)'); +// An event used to fetch the HASS object. See "Special note" below. +class HASSRequestEvent extends Event { + public hass?: ExtendedHomeAssistant; +} /** * A manager to maintain/fetch timeline events. @@ -117,17 +120,6 @@ class TimelineDataManager { // than this). protected _maxAgeSeconds: number = TIMELINE_DATA_MANAGER_MAX_AGE_SECONDS; - protected _contentCallback?: (source: FrigateBrowseMediaSource) => string; - protected _tooltipCallback?: (source: FrigateBrowseMediaSource) => string; - - constructor(params?: { - contentCallback?: (source: FrigateBrowseMediaSource) => string; - tooltipCallback?: (source: FrigateBrowseMediaSource) => string; - }) { - this._contentCallback = params?.contentCallback; - this._tooltipCallback = params?.tooltipCallback; - } - // Get the last event fetch date. get lastFetchDate(): Date | null { return this._dateFetch ?? null; @@ -178,8 +170,7 @@ class TimelineDataManager { item = { id: event.id, group: camera, - content: this._contentCallback?.(child) ?? '', - title: this._tooltipCallback?.(child) ?? '', + content: '', start: event.start_time * 1000, event: event, }; @@ -423,6 +414,62 @@ class TimelineDataManager { } } +/** + * A simgple thumbnail wrapper class for use in the timeline where LIT data + * bindings are not available. + */ +@customElement('frigate-card-timeline-thumbnail') +export class FrigateCardTimelineThumbnail extends LitElement { + @property({ attribute: true }) + public thumbnail?: string; + + @property({ attribute: true, type: Boolean }) + public details = false; + + @property({ attribute: true }) + public event?: string; + + @property({ attribute: true }) + public label?: string; + + /** + * Master render method. + * @returns A rendered template. + */ + protected render(): TemplateResult | void { + // Don't display tooltips on touch devices, they just get in the way of + // the drawer. + if (!this.thumbnail || !this.event) { + return html``; + } + + /* Special note on what's going on here: + * + * This component does not have access to HASS, as there's no way to pass it + * in via the string-based tooltip that timeline supports. Instead dispatch + * an event to request HASS which the timeline adds to the event object + * before execution continues. + */ + const hassRequest = new HASSRequestEvent(`frigate-card:timeline:hass-request`, { + composed: true, + bubbles: true, + }); + this.dispatchEvent(hassRequest); + if (!hassRequest.hass) { + return html``; + } + + return html` + `; + } +} + @customElement('frigate-card-timeline') export class FrigateCardTimeline extends LitElement { @property({ attribute: false }) @@ -484,9 +531,8 @@ export class FrigateCardTimelineCore extends LitElement { @property({ attribute: false }) public timelineConfig?: TimelineConfig; - protected _data = new TimelineDataManager({ - tooltipCallback: this._getTooltip.bind(this), - }); + protected _data = new TimelineDataManager(); + protected _refTimeline: Ref = createRef(); protected _timeline?: Timeline; @@ -495,13 +541,18 @@ export class FrigateCardTimelineCore extends LitElement { protected _pointerHeld = false; protected _ignoreClick = false; + protected static _isHoverableDevice = window.matchMedia( + '(hover: hover) and (pointer: fine)', + ).matches; + /** * Get a tooltip for a given timeline event. * @param source The FrigateBrowseMediaSource in question. * @returns The tooltip as a string to render. */ - protected _getTooltip(source: FrigateBrowseMediaSource): string { - if (!isHoverableDevice.matches) { + protected _getTooltip(item: TimelineItem): string { + const source = (item).source; + if (!FrigateCardTimelineCore._isHoverableDevice || !source) { // Don't display tooltips on touch devices, they just get in the way of // the drawer. return ''; @@ -518,13 +569,13 @@ export class FrigateCardTimelineCore extends LitElement { // Note that changes to attributes here must be mirrored in the xss // whitelist in `_getOptions()` . return ` - - `; + `; } /** @@ -544,6 +595,9 @@ export class FrigateCardTimelineCore extends LitElement { }; return html`
{ + request.hass = this.hass; + }} class="${classMap(timelineClasses)}" ${ref(this._refTimeline)} >
`; @@ -735,7 +789,7 @@ export class FrigateCardTimelineCore extends LitElement { properties: TimelineEventPropertiesResult, ): void { if (properties.event && this._pointerHeld) { - // An event will have been set when it's a human changes the range, + // An event will have been set when it's a human changes the range. this._ignoreClick = true; } } @@ -1052,12 +1106,18 @@ export class FrigateCardTimelineCore extends LitElement { tooltip: { followMouse: true, overflowMethod: 'cap', + template: this._getTooltip.bind(this), }, xss: { disabled: false, filterOptions: { whiteList: { - 'frigate-card-thumbnail': ['details', 'thumbnail', 'label', 'event'], + 'frigate-card-timeline-thumbnail': [ + 'details', + 'thumbnail', + 'label', + 'event', + ], div: ['title'], span: ['style'], }, @@ -1229,14 +1289,9 @@ export class FrigateCardTimelineCore extends LitElement { // Don't show an empty timeline, show a message instead. const groups = this._getGroups(); if (!groups.length) { - dispatchMessageEvent( - this, - localize('error.timeline_no_cameras'), - 'info', - { - icon: 'mdi:chart-gantt', - } - ); + dispatchMessageEvent(this, localize('error.timeline_no_cameras'), 'info', { + icon: 'mdi:chart-gantt', + }); return; } @@ -1257,10 +1312,10 @@ export class FrigateCardTimelineCore extends LitElement { this._timeline.on('mouseDown', () => { this._pointerHeld = true; this._ignoreClick = false; - }) + }); this._timeline.on('mouseUp', () => { this._pointerHeld = false; - }) + }); } } @@ -1279,6 +1334,7 @@ export class FrigateCardTimelineCore extends LitElement { declare global { interface HTMLElementTagNameMap { + 'frigate-card-timeline-thumbnail': FrigateCardTimelineThumbnail; 'frigate-card-timeline-core': FrigateCardTimelineCore; 'frigate-card-timeline': FrigateCardTimeline; } diff --git a/src/scss/thumbnail-feature-event.scss b/src/scss/thumbnail-feature-event.scss index 88a9bbfb..04df7733 100644 --- a/src/scss/thumbnail-feature-event.scss +++ b/src/scss/thumbnail-feature-event.scss @@ -8,7 +8,8 @@ img { } img, -ha-icon { +ha-icon, +frigate-card-progress-indicator { border-radius: var(--ha-card-border-radius, 4px); max-width: var(--frigate-card-thumbnail-size);