Allow view change events to trigger re-render.

This commit is contained in:
Dermot Duffy
2022-01-14 21:31:16 -08:00
parent d064b6ea91
commit 16ac4146d7
9 changed files with 119 additions and 68 deletions
+4
View File
@@ -4,6 +4,7 @@ import { View } from './view';
export interface ConditionState { export interface ConditionState {
view?: View; view?: View;
fullscreen?: boolean; fullscreen?: boolean;
camera?: string;
} }
class ConditionStateRequestEvent extends Event { class ConditionStateRequestEvent extends Event {
@@ -21,6 +22,9 @@ export function evaluateCondition(
if (condition?.fullscreen !== undefined && state?.fullscreen !== undefined) { if (condition?.fullscreen !== undefined && state?.fullscreen !== undefined) {
result &&= condition?.fullscreen == state?.fullscreen; result &&= condition?.fullscreen == state?.fullscreen;
} }
if (condition?.camera?.length && state?.camera) {
result &&= condition?.camera.includes(state?.camera);
}
return result; return result;
} }
+8 -8
View File
@@ -47,10 +47,11 @@ import {
convertActionToFrigateCardCustomAction, convertActionToFrigateCardCustomAction,
createFrigateCardCustomAction, createFrigateCardCustomAction,
getActionConfigGivenAction, getActionConfigGivenAction,
getCameraIcon,
getCameraTitle,
homeAssistantSignPath, homeAssistantSignPath,
homeAssistantWSRequest, homeAssistantWSRequest,
isValidMediaShowInfo, isValidMediaShowInfo,
refreshCameraConfigDynamicParameters,
shouldUpdateBasedOnHass, shouldUpdateBasedOnHass,
} from './common.js'; } from './common.js';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
@@ -222,6 +223,7 @@ export class FrigateCard extends LitElement {
this._conditionState = { this._conditionState = {
view: this._view, view: this._view,
fullscreen: screenfull.isEnabled && screenfull.isFullscreen, fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
camera: this._view?.camera,
}; };
} }
@@ -279,15 +281,11 @@ export class FrigateCard extends LitElement {
if (this.config.menu.buttons.cameras && this._cameras && this._cameras.size > 1) { if (this.config.menu.buttons.cameras && this._cameras && this._cameras.size > 1) {
const menuItems = Array.from(this._cameras, ([camera, config]) => { const menuItems = Array.from(this._cameras, ([camera, config]) => {
const dynamicConfig = refreshCameraConfigDynamicParameters(
{ ...config },
this._hass,
);
return { return {
icon: dynamicConfig.icon || 'mdi:cctv', icon: getCameraIcon(this._hass, config),
entity: dynamicConfig.camera_entity, entity: config.camera_entity,
state_color: true, state_color: true,
title: dynamicConfig.title, title: getCameraTitle(this._hass, config),
tap_action: createFrigateCardCustomAction('camera_select', camera), tap_action: createFrigateCardCustomAction('camera_select', camera),
}; };
}); });
@@ -602,6 +600,8 @@ export class FrigateCard extends LitElement {
} }
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void { protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
console.info(`Request to change view: ${JSON.stringify(args?.view)}`)
if (args?.resetMessage ?? true) { if (args?.resetMessage ?? true) {
this._message = null; this._message = null;
} }
+33 -16
View File
@@ -421,7 +421,10 @@ export function prettifyFrigateName(input?: string): string | undefined {
* @param hass The Home Assistant object. * @param hass The Home Assistant object.
* @returns The title or undefined. * @returns The title or undefined.
*/ */
export function getEntityTitle(hass?: HomeAssistant, entity?: string): string | undefined { export function getEntityTitle(
hass?: HomeAssistant,
entity?: string,
): string | undefined {
return entity ? hass?.states[entity]?.attributes?.friendly_name : undefined; return entity ? hass?.states[entity]?.attributes?.friendly_name : undefined;
} }
@@ -431,28 +434,42 @@ export function getEntityTitle(hass?: HomeAssistant, entity?: string): string |
* @param hass The Home Assistant object. * @param hass The Home Assistant object.
* @returns The icon or undefined. * @returns The icon or undefined.
*/ */
export function getEntityIcon(hass?: HomeAssistant, entity?: string): string | undefined { export function getEntityIcon(
hass?: HomeAssistant,
entity?: string,
): string | undefined {
return hass && entity ? stateIcon(hass.states[entity]) : undefined; return hass && entity ? stateIcon(hass.states[entity]) : undefined;
} }
/** /**
* Refresh a camera config with the latest Home Assistant state. * Get a camera text title.
* @param config The Camera config.
* @param hass The Home Assistant object. * @param hass The Home Assistant object.
* @returns A camera config modified in place. * @param config The camera config.
* @returns A title string.
*/ */
export function refreshCameraConfigDynamicParameters( export function getCameraTitle(
config: CameraConfig,
hass?: HomeAssistant, hass?: HomeAssistant,
): CameraConfig { config?: CameraConfig | null,
config.title = ): string {
config.title || return (
(config.camera_entity ? getEntityTitle(hass, config.camera_entity) : '') || config?.title ||
(config.camera_name ? prettifyFrigateName(config.camera_name) : ''); (config?.camera_entity ? getEntityTitle(hass, config.camera_entity) : '') ||
config.icon = (config?.camera_name ? prettifyFrigateName(config.camera_name) : '') ||
config.icon || ''
(config.camera_entity ? getEntityIcon(hass, config.camera_entity) : 'mdi:video'); );
return config; }
/**
* Get a camera icon.
* @param hass The Home Assistant object.
* @param config The camera config.
* @returns An icon string.
*/
export function getCameraIcon(
hass?: HomeAssistant,
config?: CameraConfig | null,
): string {
return config?.icon || getEntityIcon(hass, config?.camera_entity) || 'mdi:video';
} }
/** /**
+17 -4
View File
@@ -35,9 +35,22 @@ export class FrigateCardCarousel extends LitElement {
updated(changedProperties: PropertyValues): void { updated(changedProperties: PropertyValues): void {
super.updated(changedProperties); super.updated(changedProperties);
this.updateComplete.then(() => { if (this._shouldInitCarousel(changedProperties)) {
this._loadCarousel(); this.updateComplete.then(() => {
}); this._initCarousel();
});
}
}
/**
* Whether or not the carousel should be (re-)initialized when the given
* properties change.
* @param changedProperties The properties that triggered the (re-)render.
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _shouldInitCarousel(_: PropertyValues): boolean {
return true;
} }
/** /**
@@ -51,7 +64,7 @@ export class FrigateCardCarousel extends LitElement {
/** /**
* Load the carousel with "slides". * Load the carousel with "slides".
*/ */
protected _loadCarousel(): void { protected _initCarousel(): void {
const carouselNode = this.renderRoot.querySelector( const carouselNode = this.renderRoot.querySelector(
'.embla__viewport', '.embla__viewport',
) as HTMLElement; ) as HTMLElement;
+41 -30
View File
@@ -1,11 +1,15 @@
// TODO double media load event for webrtc
// TODO webrtc entities in camera section?
// TODO conditional elements based on camera name (requires event changed to propagate upwards) // TODO conditional elements based on camera name (requires event changed to propagate upwards)
// TODO Remove media load event warning // TODO _shouldInitCarousel on viewer carousel and thumbnail carousel.
// TODO call change-event in viewer
// TODO editor for live lazy loading
// TODO different live configs per camera
// TODO verify preload behavior
// TODO Remove media load event console message
// TODO Remove view change console message
// TODO readme // TODO readme
// TODO search for TODOs // TODO search for TODOs
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit';
import { import {
BrowseMediaSource, BrowseMediaSource,
ExtendedHomeAssistant, ExtendedHomeAssistant,
@@ -14,7 +18,6 @@ import {
LiveConfig, LiveConfig,
MediaShowInfo, MediaShowInfo,
WebRTCConfig, WebRTCConfig,
StateParameters,
FrigateCardError, FrigateCardError,
} from '../types.js'; } from '../types.js';
import { EmblaOptionsType } from 'embla-carousel'; import { EmblaOptionsType } from 'embla-carousel';
@@ -36,8 +39,9 @@ import {
dispatchMessageEvent, dispatchMessageEvent,
dispatchPauseEvent, dispatchPauseEvent,
dispatchPlayEvent, dispatchPlayEvent,
getCameraIcon,
getCameraTitle,
homeAssistantSignPath, homeAssistantSignPath,
refreshCameraConfigDynamicParameters,
} from '../common.js'; } from '../common.js';
import { renderProgressIndicator } from '../components/message.js'; import { renderProgressIndicator } from '../components/message.js';
@@ -203,6 +207,22 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
@property({ attribute: false }) @property({ attribute: false })
protected liveConfig?: LiveConfig; protected liveConfig?: LiveConfig;
/**
* Whether or not the carousel should be (re-)initialized when the given
* properties change.
* @param changedProperties The properties that triggered the (re-)render.
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _shouldInitCarousel(changedProps: PropertyValues): boolean {
// These are the only properties that would cause new cameras or changed
// dimensions. Don't allow other properties to re-initialize the carousel as
// it's a jarring experience to the user (and 'view' is itself set as a
// result of a carousel move).
return (changedProps.has('cameras') || changedProps.has('liveConfig'));
}
/** /**
* Get the Embla options to use. * Get the Embla options to use.
* @returns An EmblaOptionsType object or undefined for no options. * @returns An EmblaOptionsType object or undefined for no options.
@@ -240,12 +260,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
return []; return [];
} }
return Array.from(this.cameras.values()).map((cameraConfig, index) => { return Array.from(this.cameras.values()).map((cameraConfig, index) => {
let refreshedConfig = { ...cameraConfig }; return this._renderLive(cameraConfig, index);
refreshedConfig = refreshCameraConfigDynamicParameters(
refreshedConfig,
this.hass,
);
return this._renderLive(refreshedConfig, index);
}); });
} }
@@ -258,7 +273,10 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
} }
const selectedSnap = this._carousel.selectedScrollSnap(); const selectedSnap = this._carousel.selectedScrollSnap();
this.view.camera = Array.from(this.cameras.keys())[selectedSnap]; const newView = this.view.clone();
newView.camera = Array.from(this.cameras.keys())[selectedSnap];
newView.previous = this.view;
newView.dispatchChangeEvent(this);
} }
/** /**
@@ -277,7 +295,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
protected _renderLive(cameraConfig: CameraConfig, slideIndex: number): TemplateResult { protected _renderLive(cameraConfig: CameraConfig, slideIndex: number): TemplateResult {
return html` <div class="embla__slide"> return html` <div class="embla__slide">
<frigate-card-live-provider <frigate-card-live-provider
.title=${cameraConfig.title ?? ''} .title=${getCameraTitle(this.hass, cameraConfig)}
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${cameraConfig} .cameraConfig=${cameraConfig}
.liveConfig=${this.liveConfig} .liveConfig=${this.liveConfig}
@@ -289,7 +307,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
</div>`; </div>`;
} }
protected _getCameraNeighbors(): [StateParameters | null, StateParameters | null] { protected _getCameraNeighbors(): [CameraConfig | null, CameraConfig | null] {
if (!this.cameras || !this.view || !this.hass) { if (!this.cameras || !this.view || !this.hass) {
return [null, null]; return [null, null];
} }
@@ -304,15 +322,9 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
next: CameraConfig | null = null; next: CameraConfig | null = null;
if (currentIndex > 0) { if (currentIndex > 0) {
prev = this.cameras.get(keys[currentIndex - 1]) ?? null; prev = this.cameras.get(keys[currentIndex - 1]) ?? null;
if (prev) {
prev = refreshCameraConfigDynamicParameters({ ...prev }, this.hass);
}
} }
if (currentIndex + 1 < this.cameras.size) { if (currentIndex + 1 < this.cameras.size) {
next = this.cameras.get(keys[currentIndex + 1]) ?? null; next = this.cameras.get(keys[currentIndex + 1]) ?? null;
if (next) {
next = refreshCameraConfigDynamicParameters({ ...next }, this.hass);
}
} }
return [prev, next]; return [prev, next];
} }
@@ -329,8 +341,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
const target = direction == 'previous' ? prev : next; const target = direction == 'previous' ? prev : next;
control.disabled = target == null; control.disabled = target == null;
control.title = target && target.title ? target.title : ''; control.title = getCameraTitle(this.hass, target)
control.icon = target && target.icon ? target.icon : undefined; control.icon = getCameraIcon(this.hass, target);
}; };
if (this._previousControlRef.value) { if (this._previousControlRef.value) {
@@ -352,15 +364,14 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
} }
const [prev, next] = this._getCameraNeighbors(); const [prev, next] = this._getCameraNeighbors();
return html` return html`
<div class="embla"> <div class="embla">
<frigate-card-next-previous-control <frigate-card-next-previous-control
${ref(this._previousControlRef)} ${ref(this._previousControlRef)}
.direction=${'previous'} .direction=${'previous'}
.controlConfig=${this.liveConfig?.controls.next_previous} .controlConfig=${this.liveConfig?.controls.next_previous}
.title=${prev && prev.title ? prev.title : ''} .title=${getCameraTitle(this.hass, prev)}
.icon=${prev && prev.icon ? prev.icon : undefined} .icon=${getCameraIcon(this.hass, prev)}
?disabled=${prev == null} ?disabled=${prev == null}
@click=${() => { @click=${() => {
this._nextPreviousHandler('previous'); this._nextPreviousHandler('previous');
@@ -374,8 +385,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
${ref(this._nextControlRef)} ${ref(this._nextControlRef)}
.direction=${'next'} .direction=${'next'}
.controlConfig=${this.liveConfig?.controls.next_previous} .controlConfig=${this.liveConfig?.controls.next_previous}
.title=${next && next.title ? next.title : ''} .title=${getCameraTitle(this.hass, next)}
.icon=${next && next.icon ? next.icon : undefined} .icon=${getCameraIcon(this.hass, next)}
?disabled=${next == null} ?disabled=${next == null}
@click=${() => { @click=${() => {
this._nextPreviousHandler('next'); this._nextPreviousHandler('next');
@@ -423,7 +434,7 @@ export class FrigateCardLiveProvider extends LitElement {
? html`<frigate-card-live-webrtc ? html`<frigate-card-live-webrtc
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
.webRTCConfig=${this.liveConfig.webrtc || {}} .webRTCConfig=${this.liveConfig.webrtc}
> >
</frigate-card-live-webrtc>` </frigate-card-live-webrtc>`
: html` <frigate-card-live-jsmpeg : html` <frigate-card-live-jsmpeg
@@ -541,7 +552,7 @@ export class FrigateCardLiveWebRTC extends LitElement {
/** /**
* Updated lifecycle callback. * Updated lifecycle callback.
*/ */
public updated(): void { public updated(changedProps: PropertyValues): void {
// Extract the video component after it has been rendered and generate the // Extract the video component after it has been rendered and generate the
// media load event. // media load event.
this.updateComplete.then(() => { this.updateComplete.then(() => {
+4 -4
View File
@@ -44,13 +44,13 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
} }
/** /**
* Load the carousel with "slides" (clips or snapshots). * Initializse the carousel with "slides" (clips or snapshots).
*/ */
protected _loadCarousel(): void { protected _initCarousel(): void {
super._loadCarousel(); super._initCarousel();
// Necessary because typescript local type narrowing is not paying attention // Necessary because typescript local type narrowing is not paying attention
// to the side-effect of the call to super._loadCarousel(). // to the side-effect of the call to super._initCarousel().
const carousel = this._carousel as EmblaCarouselType | undefined; const carousel = this._carousel as EmblaCarouselType | undefined;
// Update the view object as the carousel is moved. // Update the view object as the carousel is moved.
-5
View File
@@ -250,11 +250,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
`; `;
} }
// export function refreshCameraConfigDynamicParameters(
// config: CameraConfig,
// hass?: HomeAssistant,
// ): CameraConfig {
/** /**
* Render a camera header. * Render a camera header.
* @param cameraIndex The index of the camera to edit/add. * @param cameraIndex The index of the camera to edit/add.
+2 -1
View File
@@ -321,6 +321,7 @@ export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
const frigateCardConditionSchema = z.object({ const frigateCardConditionSchema = z.object({
view: z.string().array().optional(), view: z.string().array().optional(),
fullscreen: z.boolean().optional(), fullscreen: z.boolean().optional(),
camera: z.string().array().optional(),
}); });
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>; export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
@@ -678,7 +679,7 @@ export const frigateCardConfigSchema = z.object({
}); });
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>; export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
export type RawFrigateCardConfig = Record<string, unknown>; export type RawFrigateCardConfig = Record<string, unknown>;
export type RawFrigateCardConfigArray = Record<string, unknown>[]; export type RawFrigateCardConfigArray = RawFrigateCardConfig[];
export const frigateCardConfigDefaults = { export const frigateCardConfigDefaults = {
cameras: cameraConfigDefault, cameras: cameraConfigDefault,
+10
View File
@@ -24,6 +24,16 @@ export class View {
this.previous = params?.previous; this.previous = params?.previous;
} }
public clone(): View {
return new View({
view: this.view,
camera: this.camera,
target: this.target,
childIndex: this.childIndex,
previous: this.previous
});
}
public is(name: string): boolean { public is(name: string): boolean {
return this.view == name; return this.view == name;
} }