diff --git a/src/card-condition.ts b/src/card-condition.ts index 8574d9bf..c93a2094 100644 --- a/src/card-condition.ts +++ b/src/card-condition.ts @@ -4,6 +4,7 @@ import { View } from './view'; export interface ConditionState { view?: View; fullscreen?: boolean; + camera?: string; } class ConditionStateRequestEvent extends Event { @@ -21,6 +22,9 @@ export function evaluateCondition( if (condition?.fullscreen !== undefined && state?.fullscreen !== undefined) { result &&= condition?.fullscreen == state?.fullscreen; } + if (condition?.camera?.length && state?.camera) { + result &&= condition?.camera.includes(state?.camera); + } return result; } diff --git a/src/card.ts b/src/card.ts index 53024f8c..ba943e93 100644 --- a/src/card.ts +++ b/src/card.ts @@ -47,10 +47,11 @@ import { convertActionToFrigateCardCustomAction, createFrigateCardCustomAction, getActionConfigGivenAction, + getCameraIcon, + getCameraTitle, homeAssistantSignPath, homeAssistantWSRequest, isValidMediaShowInfo, - refreshCameraConfigDynamicParameters, shouldUpdateBasedOnHass, } from './common.js'; import { localize } from './localize/localize.js'; @@ -222,6 +223,7 @@ export class FrigateCard extends LitElement { this._conditionState = { view: this._view, 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) { const menuItems = Array.from(this._cameras, ([camera, config]) => { - const dynamicConfig = refreshCameraConfigDynamicParameters( - { ...config }, - this._hass, - ); return { - icon: dynamicConfig.icon || 'mdi:cctv', - entity: dynamicConfig.camera_entity, + icon: getCameraIcon(this._hass, config), + entity: config.camera_entity, state_color: true, - title: dynamicConfig.title, + title: getCameraTitle(this._hass, config), tap_action: createFrigateCardCustomAction('camera_select', camera), }; }); @@ -602,6 +600,8 @@ export class FrigateCard extends LitElement { } protected _changeView(args?: { view?: View; resetMessage?: boolean }): void { + console.info(`Request to change view: ${JSON.stringify(args?.view)}`) + if (args?.resetMessage ?? true) { this._message = null; } diff --git a/src/common.ts b/src/common.ts index bf92e433..05dc3c67 100644 --- a/src/common.ts +++ b/src/common.ts @@ -421,7 +421,10 @@ export function prettifyFrigateName(input?: string): string | undefined { * @param hass The Home Assistant object. * @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; } @@ -431,28 +434,42 @@ export function getEntityTitle(hass?: HomeAssistant, entity?: string): string | * @param hass The Home Assistant object. * @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; } /** - * Refresh a camera config with the latest Home Assistant state. - * @param config The Camera config. + * Get a camera text title. * @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( - config: CameraConfig, +export function getCameraTitle( hass?: HomeAssistant, -): CameraConfig { - config.title = - config.title || - (config.camera_entity ? getEntityTitle(hass, config.camera_entity) : '') || - (config.camera_name ? prettifyFrigateName(config.camera_name) : ''); - config.icon = - config.icon || - (config.camera_entity ? getEntityIcon(hass, config.camera_entity) : 'mdi:video'); - return config; + config?: CameraConfig | null, +): string { + return ( + config?.title || + (config?.camera_entity ? getEntityTitle(hass, config.camera_entity) : '') || + (config?.camera_name ? prettifyFrigateName(config.camera_name) : '') || + '' + ); +} + +/** + * 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'; } /** diff --git a/src/components/carousel.ts b/src/components/carousel.ts index 7cbd1d47..41a43264 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -35,9 +35,22 @@ export class FrigateCardCarousel extends LitElement { updated(changedProperties: PropertyValues): void { super.updated(changedProperties); - this.updateComplete.then(() => { - this._loadCarousel(); - }); + if (this._shouldInitCarousel(changedProperties)) { + 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". */ - protected _loadCarousel(): void { + protected _initCarousel(): void { const carouselNode = this.renderRoot.querySelector( '.embla__viewport', ) as HTMLElement; diff --git a/src/components/live.ts b/src/components/live.ts index 7572d882..4d20c4d1 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -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 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 search for TODOs -import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit'; import { BrowseMediaSource, ExtendedHomeAssistant, @@ -14,7 +18,6 @@ import { LiveConfig, MediaShowInfo, WebRTCConfig, - StateParameters, FrigateCardError, } from '../types.js'; import { EmblaOptionsType } from 'embla-carousel'; @@ -36,8 +39,9 @@ import { dispatchMessageEvent, dispatchPauseEvent, dispatchPlayEvent, + getCameraIcon, + getCameraTitle, homeAssistantSignPath, - refreshCameraConfigDynamicParameters, } from '../common.js'; import { renderProgressIndicator } from '../components/message.js'; @@ -203,6 +207,22 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { @property({ attribute: false }) 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. * @returns An EmblaOptionsType object or undefined for no options. @@ -240,12 +260,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { return []; } return Array.from(this.cameras.values()).map((cameraConfig, index) => { - let refreshedConfig = { ...cameraConfig }; - refreshedConfig = refreshCameraConfigDynamicParameters( - refreshedConfig, - this.hass, - ); - return this._renderLive(refreshedConfig, index); + return this._renderLive(cameraConfig, index); }); } @@ -258,7 +273,10 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { } 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 { return html`