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`
`; } - protected _getCameraNeighbors(): [StateParameters | null, StateParameters | null] { + protected _getCameraNeighbors(): [CameraConfig | null, CameraConfig | null] { if (!this.cameras || !this.view || !this.hass) { return [null, null]; } @@ -304,15 +322,9 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { next: CameraConfig | null = null; if (currentIndex > 0) { prev = this.cameras.get(keys[currentIndex - 1]) ?? null; - if (prev) { - prev = refreshCameraConfigDynamicParameters({ ...prev }, this.hass); - } } if (currentIndex + 1 < this.cameras.size) { next = this.cameras.get(keys[currentIndex + 1]) ?? null; - if (next) { - next = refreshCameraConfigDynamicParameters({ ...next }, this.hass); - } } return [prev, next]; } @@ -329,8 +341,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { const target = direction == 'previous' ? prev : next; control.disabled = target == null; - control.title = target && target.title ? target.title : ''; - control.icon = target && target.icon ? target.icon : undefined; + control.title = getCameraTitle(this.hass, target) + control.icon = getCameraIcon(this.hass, target); }; if (this._previousControlRef.value) { @@ -352,15 +364,14 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { } const [prev, next] = this._getCameraNeighbors(); - return html`
{ this._nextPreviousHandler('previous'); @@ -374,8 +385,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { ${ref(this._nextControlRef)} .direction=${'next'} .controlConfig=${this.liveConfig?.controls.next_previous} - .title=${next && next.title ? next.title : ''} - .icon=${next && next.icon ? next.icon : undefined} + .title=${getCameraTitle(this.hass, next)} + .icon=${getCameraIcon(this.hass, next)} ?disabled=${next == null} @click=${() => { this._nextPreviousHandler('next'); @@ -423,7 +434,7 @@ export class FrigateCardLiveProvider extends LitElement { ? html` ` : html` { diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index de31a1df..2959f2b6 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -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 { - super._loadCarousel(); + protected _initCarousel(): void { + super._initCarousel(); // 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; // Update the view object as the carousel is moved. diff --git a/src/editor.ts b/src/editor.ts index 2ee97ae3..3b016ebe 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -250,11 +250,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor `; } - // export function refreshCameraConfigDynamicParameters( - // config: CameraConfig, - // hass?: HomeAssistant, - // ): CameraConfig { - /** * Render a camera header. * @param cameraIndex The index of the camera to edit/add. diff --git a/src/types.ts b/src/types.ts index 4d85006f..b822e2e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -321,6 +321,7 @@ export type MenuSubmenu = z.infer; const frigateCardConditionSchema = z.object({ view: z.string().array().optional(), fullscreen: z.boolean().optional(), + camera: z.string().array().optional(), }); export type FrigateCardCondition = z.infer; @@ -678,7 +679,7 @@ export const frigateCardConfigSchema = z.object({ }); export type FrigateCardConfig = z.infer; export type RawFrigateCardConfig = Record; -export type RawFrigateCardConfigArray = Record[]; +export type RawFrigateCardConfigArray = RawFrigateCardConfig[]; export const frigateCardConfigDefaults = { cameras: cameraConfigDefault, diff --git a/src/view.ts b/src/view.ts index 313bf0c7..4843d28f 100644 --- a/src/view.ts +++ b/src/view.ts @@ -24,6 +24,16 @@ export class View { 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 { return this.view == name; }