From af09473e3ee7acc7a11b9da2f96917264d4dad60 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 6 Aug 2022 10:50:03 -0700 Subject: [PATCH] Fetch thumbnails for next/previous controls. --- src/components/live.ts | 2 + src/components/next-prev-control.ts | 49 ++++++++++++++++---- src/components/thumbnail.ts | 48 ++----------------- src/components/viewer.ts | 2 + src/utils/thumbnail.ts | 71 +++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 53 deletions(-) create mode 100644 src/utils/thumbnail.ts diff --git a/src/components/live.ts b/src/components/live.ts index 9f29bf85..92fa484c 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -585,6 +585,7 @@ export class FrigateCardLiveCarousel extends LitElement { > this.hass, + () => this.thumbnail, + ); + protected render(): TemplateResult { if (this.disabled || !this._controlConfig || this._controlConfig.style == 'none') { return html``; @@ -66,12 +79,30 @@ export class FrigateCardNextPreviousControl extends LitElement { if (!this.thumbnail) { return html``; } - return html``; + + const renderControlInProgress = (): TemplateResult => { + // Just render an 'empty' thumbnail control until the thumbnail loads. + return html`
`; + }; + + return html`${this._embedThumbnailTask.render({ + initial: () => renderControlInProgress(), + pending: () => renderControlInProgress(), + error: (e: unknown) => { + errorToConsole(e as Error); + dispatchFrigateCardErrorEvent(this, e as Error); + }, + complete: (embeddedThumbnail: string | null) => { + return embeddedThumbnail + ? html`` + : html``; + }, + })}`; } static get styles(): CSSResultGroup { @@ -80,7 +111,7 @@ export class FrigateCardNextPreviousControl extends LitElement { } declare global { - interface HTMLElementTagNameMap { - "frigate-card-next-previous-control": FrigateCardNextPreviousControl - } + interface HTMLElementTagNameMap { + 'frigate-card-next-previous-control': FrigateCardNextPreviousControl; + } } diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index ddd1a2e6..5617e729 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -1,4 +1,3 @@ -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'; @@ -18,6 +17,7 @@ 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 { createFetchThumbnailTask } from '../utils/thumbnail.js'; import { View } from '../view.js'; import { dispatchFrigateCardErrorEvent, renderProgressIndicator } from './message.js'; @@ -32,52 +32,12 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement { @property({ attribute: false }) public hass?: ExtendedHomeAssistant; - protected _embedThumbnailTask = new Task( + protected _embedThumbnailTask = createFetchThumbnailTask( this, - this._embedThumbnail.bind(this), - // Do not re-run the task if hass changes, unless it was previously undefined. - (): [boolean, string | undefined] => [!!this.hass, this.thumbnail], + () => this.hass, + () => this.thumbnail, ); - /** - * Sign a thumbnail URL if necessary. May throw. - * @param param0 A list of lit-task dependencies. - * @returns A signed URL or null. - */ - protected async _embedThumbnail([haveHASS, thumbnail]: [ - boolean, - string | undefined, - ]): Promise { - if (!haveHASS || !this.hass || !thumbnail) { - return null; - } - if (this.thumbnail?.startsWith('data:')) { - return this.thumbnail; - } - return new Promise((resolve, reject) => { - if (!this.hass) { - reject(); - return; - } - this.hass - .fetchWithAuth(thumbnail) - // Since we are fetching with an authorization header, we cannot just put the - // URL directly into the document; we need to embed the image. We could do this - // using blob URLs, but then we would need to keep track of them in order to - // release them properly. Instead, we embed the thumbnail using base64. - .then((response) => response.blob()) - .then((blob) => { - const reader = new FileReader(); - reader.onload = () => { - const result = reader.result; - resolve(typeof result === 'string' ? result : null); - }; - reader.onerror = (e) => reject(e); - reader.readAsDataURL(blob); - }); - }); - } - protected render(): TemplateResult | void { return html` ${this.thumbnail diff --git a/src/components/viewer.ts b/src/components/viewer.ts index c55ee3ad..dc8c8185 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -627,6 +627,7 @@ export class FrigateCardViewerCarousel extends LitElement { > => { + if (!hass) { + return null; + } + if (thumbnailURL?.startsWith('data:')) { + return thumbnailURL; + } + return new Promise((resolve, reject) => { + if (!hass) { + reject(); + return; + } + hass + .fetchWithAuth(thumbnailURL) + // Since we are fetching with an authorization header, we cannot just put the + // URL directly into the document; we need to embed the image. We could do this + // using blob URLs, but then we would need to keep track of them in order to + // release them properly. Instead, we embed the thumbnail using base64. + .then((response) => response.blob()) + .then((blob) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result; + resolve(typeof result === 'string' ? result : null); + }; + reader.onerror = (e) => reject(e); + reader.readAsDataURL(blob); + }); + }); +}; + +/** + * Create a Lit task to fetch a thumbnail. + * @param host The Lit Element. + * @param getHASS A function to get the Home Assistant object. + * @param getThumbnail A function to get the Thumbnail URL. + * @returns A new Lit Task. + */ +export const createFetchThumbnailTask = ( + host: ReactiveControllerHost, + getHASS: () => HomeAssistant | undefined, + getThumbnailURL: () => string | undefined, +): Task => { + return new Task( + host, + async ([haveHASS, thumbnailURL]: [boolean, string | undefined]): Promise< + string | null + > => { + const hass = getHASS(); + if (!haveHASS || !hass || !thumbnailURL) { + return null; + } + return fetchThumbnail(hass, thumbnailURL); + }, + // Do not re-run the task if hass changes, unless it was previously undefined. + (): [boolean, string | undefined] => [!!getHASS(), getThumbnailURL()], + ); +};