Fetch thumbnails for next/previous controls.
This commit is contained in:
@@ -585,6 +585,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
>
|
>
|
||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
slot="previous"
|
slot="previous"
|
||||||
|
.hass=${this.hass}
|
||||||
.direction=${'previous'}
|
.direction=${'previous'}
|
||||||
.controlConfig=${config.controls.next_previous}
|
.controlConfig=${config.controls.next_previous}
|
||||||
.label=${getCameraTitle(this.hass, prev)}
|
.label=${getCameraTitle(this.hass, prev)}
|
||||||
@@ -601,6 +602,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
${slides}
|
${slides}
|
||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
slot="next"
|
slot="next"
|
||||||
|
.hass=${this.hass}
|
||||||
.direction=${'next'}
|
.direction=${'next'}
|
||||||
.controlConfig=${config.controls.next_previous}
|
.controlConfig=${config.controls.next_previous}
|
||||||
.label=${getCameraTitle(this.hass, next)}
|
.label=${getCameraTitle(this.hass, next)}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { NextPreviousControlConfig } from '../types.js';
|
import { NextPreviousControlConfig } from '../types.js';
|
||||||
|
|
||||||
import controlStyle from '../scss/next-previous-control.scss';
|
import controlStyle from '../scss/next-previous-control.scss';
|
||||||
|
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||||
|
import { errorToConsole } from '../utils/basic.js';
|
||||||
|
|
||||||
@customElement('frigate-card-next-previous-control')
|
@customElement('frigate-card-next-previous-control')
|
||||||
export class FrigateCardNextPreviousControl extends LitElement {
|
export class FrigateCardNextPreviousControl extends LitElement {
|
||||||
@@ -18,6 +22,9 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
this._controlConfig = controlConfig;
|
this._controlConfig = controlConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _controlConfig?: NextPreviousControlConfig;
|
protected _controlConfig?: NextPreviousControlConfig;
|
||||||
|
|
||||||
@@ -33,6 +40,12 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
// Label that is used for ARIA support and as tooltip.
|
// Label that is used for ARIA support and as tooltip.
|
||||||
@property() label = '';
|
@property() label = '';
|
||||||
|
|
||||||
|
protected _embedThumbnailTask = createFetchThumbnailTask(
|
||||||
|
this,
|
||||||
|
() => this.hass,
|
||||||
|
() => this.thumbnail,
|
||||||
|
);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (this.disabled || !this._controlConfig || this._controlConfig.style == 'none') {
|
if (this.disabled || !this._controlConfig || this._controlConfig.style == 'none') {
|
||||||
return html``;
|
return html``;
|
||||||
@@ -66,12 +79,30 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
if (!this.thumbnail) {
|
if (!this.thumbnail) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
return html`<img
|
|
||||||
src="${this.thumbnail}"
|
const renderControlInProgress = (): TemplateResult => {
|
||||||
class="${classMap(classes)}"
|
// Just render an 'empty' thumbnail control until the thumbnail loads.
|
||||||
title="${this.label}"
|
return html`<div class=${classMap(classes)}></div>`;
|
||||||
aria-label="${this.label}"
|
};
|
||||||
/>`;
|
|
||||||
|
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`<img
|
||||||
|
src="${embeddedThumbnail}"
|
||||||
|
class="${classMap(classes)}"
|
||||||
|
title="${this.label}"
|
||||||
|
aria-label="${this.label}"
|
||||||
|
/>`
|
||||||
|
: html``;
|
||||||
|
},
|
||||||
|
})}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -80,7 +111,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"frigate-card-next-previous-control": FrigateCardNextPreviousControl
|
'frigate-card-next-previous-control': FrigateCardNextPreviousControl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Task } from '@lit-labs/task/task.js';
|
|
||||||
import { format, fromUnixTime } from 'date-fns';
|
import { format, fromUnixTime } from 'date-fns';
|
||||||
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
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 { errorToConsole, prettifyTitle } from '../utils/basic.js';
|
||||||
import { retainEvent } from '../utils/frigate.js';
|
import { retainEvent } from '../utils/frigate.js';
|
||||||
import { getEventDurationString } from '../utils/ha/browse-media.js';
|
import { getEventDurationString } from '../utils/ha/browse-media.js';
|
||||||
|
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { dispatchFrigateCardErrorEvent, renderProgressIndicator } from './message.js';
|
import { dispatchFrigateCardErrorEvent, renderProgressIndicator } from './message.js';
|
||||||
|
|
||||||
@@ -32,52 +32,12 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
protected _embedThumbnailTask = new Task(
|
protected _embedThumbnailTask = createFetchThumbnailTask(
|
||||||
this,
|
this,
|
||||||
this._embedThumbnail.bind(this),
|
() => this.hass,
|
||||||
// Do not re-run the task if hass changes, unless it was previously undefined.
|
() => this.thumbnail,
|
||||||
(): [boolean, string | undefined] => [!!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<string | null> {
|
|
||||||
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 {
|
protected render(): TemplateResult | void {
|
||||||
return html`
|
return html`
|
||||||
${this.thumbnail
|
${this.thumbnail
|
||||||
|
|||||||
@@ -627,6 +627,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
>
|
>
|
||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
slot="previous"
|
slot="previous"
|
||||||
|
.hass=${this.hass}
|
||||||
.direction=${'previous'}
|
.direction=${'previous'}
|
||||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||||
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
|
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
|
||||||
@@ -640,6 +641,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
${slides}
|
${slides}
|
||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
slot="next"
|
slot="next"
|
||||||
|
.hass=${this.hass}
|
||||||
.direction=${'next'}
|
.direction=${'next'}
|
||||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||||
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
|
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Task } from '@lit-labs/task';
|
||||||
|
import { ReactiveControllerHost } from '@lit/reactive-element';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a thumbnail URL and return a data URL.
|
||||||
|
* @param hass Home Assistant object.
|
||||||
|
* @param thumbnailURL The thumbnail URL.
|
||||||
|
* @returns A base64 encoded data URL for the thumbnail.
|
||||||
|
*/
|
||||||
|
export const fetchThumbnail = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
thumbnailURL: string,
|
||||||
|
): Promise<string | null> => {
|
||||||
|
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()],
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user