From e0daa17834043b91500c1d724a37537edc0b3276 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 1 Nov 2021 12:43:33 -0700 Subject: [PATCH] Initial configuration reorganization. --- src/card.ts | 108 +++++---- src/common.ts | 4 +- src/components/image.ts | 8 +- src/components/live.ts | 12 +- src/components/menu.ts | 69 +++--- src/components/next-prev-control.ts | 29 ++- src/components/viewer.ts | 94 ++++---- src/editor.ts | 335 +++++++++++++++------------- src/localize/languages/en.json | 165 ++++++++------ src/scss/viewer.scss | 5 + src/types.ts | 286 +++++++++++++++++------- 11 files changed, 638 insertions(+), 477 deletions(-) diff --git a/src/card.ts b/src/card.ts index 1374a4fc..c15199cc 100644 --- a/src/card.ts +++ b/src/card.ts @@ -192,72 +192,72 @@ export class FrigateCard extends LitElement { protected _getMenuButtons(): MenuButton[] { const buttons: MenuButton[] = []; - if (this.config.menu_buttons?.frigate ?? true) { + if (this.config.menu.buttons.frigate) { buttons.push({ type: 'internal-menu-icon', tap_action: 'frigate', - title: localize('menu.frigate'), + title: localize('config.menu.buttons.frigate'), }); } - if (this.config.menu_buttons?.live ?? true) { + if (this.config.menu.buttons.live) { buttons.push({ type: 'internal-menu-icon', tap_action: 'live', - title: localize('menu.live'), + title: localize('config.view.views.live'), icon: 'mdi:cctv', emphasize: this._view.is('live'), }); } - if (this.config.menu_buttons?.clips ?? true) { + if (this.config.menu.buttons.clips) { buttons.push({ type: 'internal-menu-icon', tap_action: 'clips', hold_action: 'clip', - title: localize('menu.clips'), + title: localize('config.view.views.clips'), icon: 'mdi:filmstrip', emphasize: this._view.is('clips'), }); } - if (this.config.menu_buttons?.snapshots ?? true) { + if (this.config.menu.buttons.snapshots) { buttons.push({ type: 'internal-menu-icon', tap_action: 'snapshots', hold_action: 'snapshot', - title: localize('menu.snapshots'), + title: localize('config.view.views.snapshots'), icon: 'mdi:camera', emphasize: this._view.is('snapshots'), }); } - if (this.config.menu_buttons?.image ?? false) { + if (this.config.menu.buttons.image) { buttons.push({ type: 'internal-menu-icon', tap_action: 'image', - title: localize('menu.image'), + title: localize('config.view.views.image'), icon: 'mdi:image', emphasize: this._view.is('image'), }); } - if (this._view.isViewerView() && (this.config.menu_buttons?.download ?? true)) { + if (this.config.menu.buttons.download && this._view.isViewerView()) { buttons.push({ type: 'internal-menu-icon', tap_action: 'download', - title: localize('menu.download'), + title: localize('config.menu.buttons.download'), icon: 'mdi:download', }); } - if ((this.config.menu_buttons?.frigate_ui ?? true) && this.config.frigate_url) { + if (this.config.menu.buttons.frigate_ui && this.config.frigate.url) { buttons.push({ type: 'internal-menu-icon', tap_action: 'frigate_ui', - title: localize('menu.frigate_ui'), + title: localize('config.menu.buttons.frigate_ui'), icon: 'mdi:web', }); } - if ((this.config.menu_buttons?.fullscreen ?? true) && screenfull.isEnabled) { + if (this.config.menu.buttons.fullscreen && screenfull.isEnabled) { buttons.push({ type: 'internal-menu-icon', tap_action: 'fullscreen', - title: localize('menu.fullscreen'), + title: localize('config.menu.buttons.fullscreen'), icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen', }); } @@ -300,8 +300,8 @@ export class FrigateCard extends LitElement { } // Option 1: Name specified in config -> done! - if (this.config.frigate_camera_name) { - return this.config.frigate_camera_name; + if (this.config.frigate.camera_name) { + return this.config.frigate.camera_name; } if (this.config.camera_entity) { @@ -436,7 +436,7 @@ export class FrigateCard extends LitElement { this._message = null; if (view === undefined) { - this._view = new View({ view: this.config.view_default }); + this._view = new View({ view: this.config.view.default }); } else { this._view = view; } @@ -505,7 +505,7 @@ export class FrigateCard extends LitElement { } const path = - `/api/frigate/${this.config.frigate_client_id}` + + `/api/frigate/${this.config.frigate.client_id}` + `/notifications/${event_id}/` + `${this._view.isClipRelatedView() ? 'clip.mp4' : 'snapshot.jpg'}` + `?download=true`; @@ -595,22 +595,22 @@ export class FrigateCard extends LitElement { * @returns The URL or null if unavailable. */ protected _getFrigateURLFromContext(): string | null { - if (!this.config.frigate_url) { + if (!this.config.frigate.url) { return null; } if (!this._frigateCameraName) { - return this.config.frigate_url; + return this.config.frigate.url; } else if (this._view.is('live')) { - return `${this.config.frigate_url}/cameras/${this._frigateCameraName}`; + return `${this.config.frigate.url}/cameras/${this._frigateCameraName}`; } - return `${this.config.frigate_url}/events?camera=${this._frigateCameraName}`; + return `${this.config.frigate.url}/events?camera=${this._frigateCameraName}`; } /** * Handle interaction with the card. */ protected _interactionHandler(): void { - if (!this.config.view_timeout) { + if (!this.config.view.timeout) { return; } if (this._interactionTimerID) { @@ -619,7 +619,7 @@ export class FrigateCard extends LitElement { this._interactionTimerID = window.setTimeout(() => { this._interactionTimerID = null; this._changeView(); - }, this.config.view_timeout * 1000); + }, this.config.view.timeout * 1000); } /** @@ -628,15 +628,14 @@ export class FrigateCard extends LitElement { */ protected _renderMenu(): TemplateResult | void { const classes = { - 'hover-menu': this.config.menu_mode.startsWith('hover-'), + 'hover-menu': this.config.menu.mode.startsWith('hover-'), }; return html` `; @@ -655,10 +654,10 @@ export class FrigateCard extends LitElement { } return { mediaType: this._view.isClipRelatedView() ? 'clips' : 'snapshots', - clientId: this.config.frigate_client_id, + clientId: this.config.frigate.client_id, cameraName: this._frigateCameraName, - label: this.config.label, - zone: this.config.zone, + label: this.config.frigate.label, + zone: this.config.frigate.zone, }; } @@ -760,7 +759,7 @@ export class FrigateCard extends LitElement { * context. */ protected _isAspectRatioEnforced(): boolean { - const aspect_ratio_mode = this.config.dimensions?.aspect_ratio_mode ?? 'dynamic'; + const aspectRatioMode = this.config.dimensions.aspect_ratio_mode; // Do not artifically constrain aspect ratio if: // - It's fullscreen. @@ -769,8 +768,8 @@ export class FrigateCard extends LitElement { return !( (screenfull.isEnabled && screenfull.isFullscreen) || - aspect_ratio_mode == 'unconstrained' || - (aspect_ratio_mode == 'dynamic' && this._view.isMediaView()) + aspectRatioMode == 'unconstrained' || + (aspectRatioMode == 'dynamic' && this._view.isMediaView()) ); } @@ -784,14 +783,14 @@ export class FrigateCard extends LitElement { return null; } - const aspect_ratio_mode = this.config.dimensions?.aspect_ratio_mode ?? 'dynamic'; - if (aspect_ratio_mode == 'dynamic' && this._mediaShowInfo) { + const aspectRatioMode = this.config.dimensions.aspect_ratio_mode; + if (aspectRatioMode == 'dynamic' && this._mediaShowInfo) { return (this._mediaShowInfo.height / this._mediaShowInfo.width) * 100; } - const default_aspect_ratio = this.config.dimensions?.aspect_ratio; - if (default_aspect_ratio) { - return (default_aspect_ratio[1] / default_aspect_ratio[0]) * 100; + const defaultAspectRatio = this.config.dimensions.aspect_ratio; + if (defaultAspectRatio) { + return (defaultAspectRatio[1] / defaultAspectRatio[0]) * 100; } else { return (9 / 16) * 100; } @@ -836,7 +835,7 @@ export class FrigateCard extends LitElement { window.innerWidth / window.innerHeight ) { // If the menu is outside the media (i.e. above/below) allow space for it. - const allowance = ['above', 'below'].includes(this.config.menu_mode) + const allowance = ['above', 'below'].includes(this.config.menu.mode) ? MENU_HEIGHT : 0; innerStyle['max-width'] = `calc(${ @@ -850,7 +849,7 @@ export class FrigateCard extends LitElement { }; return html` - ${this.config.menu_mode == 'above' ? this._renderMenu() : ''} + ${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
${this._frigateCameraName == undefined @@ -864,7 +863,7 @@ export class FrigateCard extends LitElement { : this._render()}
- ${this.config.menu_mode != 'above' ? this._renderMenu() : ''} + ${this.config.menu.mode != 'above' ? this._renderMenu() : ''}
`; } @@ -891,16 +890,16 @@ export class FrigateCard extends LitElement { gallery: this._view.isGalleryView(), }; const galleryClasses = { - hidden: this.config.live_preload && !this._view.isGalleryView(), + hidden: this.config.live.preload && !this._view.isGalleryView(), }; const viewerClasses = { - hidden: this.config.live_preload && !this._view.isViewerView(), + hidden: this.config.live.preload && !this._view.isViewerView(), }; const liveClasses = { - hidden: this.config.live_preload && this._view.view != 'live', + hidden: this.config.live.preload && this._view.view != 'live', }; const imageClasses = { - hidden: this.config.live_preload && this._view.view != 'image', + hidden: this.config.live.preload && this._view.view != 'image', }; return html` @@ -908,7 +907,7 @@ export class FrigateCard extends LitElement { ${this._message ? renderMessage(this._message) : ``} ${!this._message && this._view.is('image') ? html` { dispatchMediaShowEvent(this, e); }} diff --git a/src/components/live.ts b/src/components/live.ts index d7efd8e1..fafd5f2c 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -1,5 +1,5 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; -import type { ExtendedHomeAssistant, FrigateCardConfig, MediaShowInfo } from '../types.js'; +import type { ExtendedHomeAssistant, FrigateCardConfig, MediaShowInfo, WebRTCConfig } from '../types.js'; import { HomeAssistant } from 'custom-card-helpers'; import { customElement, property } from 'lit/decorators.js'; import { until } from 'lit/directives/until.js'; @@ -75,24 +75,24 @@ export class FrigateCardLive extends LitElement { return; } - return html` ${this.config.live_provider == 'frigate' + return html` ${this.config.live.provider == 'frigate' ? html` ` - : this.config.live_provider == 'webrtc' + : this.config.live.provider == 'webrtc' ? html` ` : html` `}`; @@ -152,7 +152,7 @@ export class FrigateCardLiveFrigate extends LitElement { @customElement('frigate-card-live-webrtc') export class FrigateCardLiveWebRTC extends LitElement { @property({ attribute: false }) - protected webRTCConfig?: Record; + protected webRTCConfig?: WebRTCConfig; protected hass?: HomeAssistant & ExtendedHomeAssistant; protected _webRTCElement: HTMLElement | null = null; diff --git a/src/components/menu.ts b/src/components/menu.ts index 484a1009..9c2e4e58 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -16,8 +16,8 @@ import { actionHandler } from '../action-handler-directive.js'; import type { ExtendedHomeAssistant, - FrigateMenuMode, MenuButton, + MenuConfig, MenuInteraction, } from '../types.js'; import { dispatchFrigateCardEvent, shouldUpdateBasedOnHass } from '../common.js'; @@ -30,10 +30,16 @@ export const MENU_HEIGHT = 46; @customElement('frigate-card-menu') export class FrigateCardMenu extends LitElement { @property({ attribute: false }) - public hass!: HomeAssistant & ExtendedHomeAssistant; + public hass?: HomeAssistant & ExtendedHomeAssistant; @property({ attribute: false }) - protected menuMode: FrigateMenuMode = 'hidden-top'; + set menuConfig(menuConfig: MenuConfig) { + this._menuConfig = menuConfig; + if (menuConfig) { + this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size); + } + } + public _menuConfig?: MenuConfig; @property({ attribute: false }) protected expand = false; @@ -41,13 +47,8 @@ export class FrigateCardMenu extends LitElement { @property({ attribute: false }) public buttons: MenuButton[] = []; - @property({ attribute: false }) - set buttonSize(buttonSize: string) { - this.style.setProperty('--frigate-card-menu-button-size', buttonSize); - } - protected _interactionHandler(ev: CustomEvent, button: MenuButton): void { - if (this.menuMode.startsWith('hidden-')) { + if (this._menuConfig?.mode.startsWith('hidden-')) { if (button.type == 'internal-menu-icon' && button.tap_action === 'frigate') { this.expand = !this.expand; return; @@ -82,7 +83,7 @@ export class FrigateCardMenu extends LitElement { } // Render a menu button. - protected _renderButton(button: MenuButton): TemplateResult { + protected _renderButton(button: MenuButton): TemplateResult | void { let state: HassEntity | null = null; let emphasize = false; let title = button.title; @@ -90,6 +91,9 @@ export class FrigateCardMenu extends LitElement { const style = ('style' in button ? button.style : {}) || {}; if (button.type === 'custom:frigate-card-menu-state-icon') { + if (!this.hass) { + return; + } state = this.hass.states[button.entity]; emphasize = !!state && button.state_color && ['on', 'active', 'home'].includes(state.state); @@ -133,9 +137,9 @@ export class FrigateCardMenu extends LitElement { } // Render the Frigate menu button. - protected _renderFrigateButton(button: MenuButton): TemplateResult { + protected _renderFrigateButton(button: MenuButton): TemplateResult | void { const icon = - this.menuMode.startsWith('hidden-') && !this.expand + this._menuConfig?.mode.startsWith('hidden-') && !this.expand ? 'mdi:alpha-f-box-outline' : 'mdi:alpha-f-box'; @@ -143,7 +147,12 @@ export class FrigateCardMenu extends LitElement { } // Render the menu. - protected render(): TemplateResult { + protected render(): TemplateResult | void { + if (!this._menuConfig) { + return; + } + const mode = this._menuConfig.mode; + const isFrigateButton = function (button: MenuButton): boolean { return button.type === 'internal-menu-icon' && button.tap_action === 'frigate'; }; @@ -151,33 +160,29 @@ export class FrigateCardMenu extends LitElement { // If the menu is off, or if it's in hidden mode but there's no button to // unhide it, just show nothing. if ( - this.menuMode == 'none' || - (this.menuMode.startsWith('hidden-') && !this.buttons.find(isFrigateButton)) + mode == 'none' || + (mode.startsWith('hidden-') && !this.buttons.find(isFrigateButton)) ) { - return html``; + return; } const classes = { 'frigate-card-menu': true, 'overlay-hidden': - this.menuMode.startsWith('hidden-') || - this.menuMode.startsWith('overlay-') || - this.menuMode.startsWith('hover-'), + mode.startsWith('hidden-') || + mode.startsWith('overlay-') || + mode.startsWith('hover-'), 'expanded-horizontal': - (this.menuMode.startsWith('overlay-') || - this.menuMode.startsWith('hover-') || - this.expand) && - (this.menuMode.endsWith('-top') || this.menuMode.endsWith('-bottom')), + (mode.startsWith('overlay-') || mode.startsWith('hover-') || this.expand) && + (mode.endsWith('-top') || mode.endsWith('-bottom')), 'expanded-vertical': - (this.menuMode.startsWith('overlay-') || - this.menuMode.startsWith('hover-') || - this.expand) && - (this.menuMode.endsWith('-left') || this.menuMode.endsWith('-right')), - full: ['above', 'below'].includes(this.menuMode), - left: this.menuMode.endsWith('-left'), - right: this.menuMode.endsWith('-right'), - top: this.menuMode.endsWith('-top'), - bottom: this.menuMode.endsWith('-bottom'), + (mode.startsWith('overlay-') || mode.startsWith('hover-') || this.expand) && + (mode.endsWith('-left') || mode.endsWith('-right')), + full: mode == 'above' || mode == 'below', + left: mode.endsWith('-left'), + right: mode.endsWith('-right'), + top: mode.endsWith('-top'), + bottom: mode.endsWith('-bottom'), }; return html` diff --git a/src/components/next-prev-control.ts b/src/components/next-prev-control.ts index 708f97ff..e9e3d68b 100644 --- a/src/components/next-prev-control.ts +++ b/src/components/next-prev-control.ts @@ -2,30 +2,29 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; -import { NextPreviousControlStyle } from '../types.js'; +import { NextPreviousControlConfig } from '../types.js'; import controlStyle from '../scss/next-previous-control.scss'; @customElement('frigate-card-next-previous-control') -export class FrigateCardMessage extends LitElement { +export class FrigateCardNextPreviousControl extends LitElement { @property({ attribute: false }) protected direction?: 'next' | 'previous'; @property({ attribute: false }) - protected controlStyle?: NextPreviousControlStyle; + set controlConfig(controlConfig: NextPreviousControlConfig | undefined) { + if (controlConfig?.size) { + this.style.setProperty('--frigate-card-next-prev-size', controlConfig.size); + } + this._controlConfig = controlConfig; + } + protected _controlConfig?: NextPreviousControlConfig; @property({ attribute: false }) protected thumbnail?: string; - @property({ attribute: false }) - set controlSize(controlSize: string | undefined) { - if (controlSize) { - this.style.setProperty('--frigate-card-next-prev-size', controlSize); - } - } - protected render(): TemplateResult { - if (!this.controlStyle || this.controlStyle == 'none') { + if (!this._controlConfig || this._controlConfig.style == 'none') { return html``; } @@ -33,12 +32,12 @@ export class FrigateCardMessage extends LitElement { controls: true, previous: this.direction == 'previous', next: this.direction == 'next', - thumbnails: this.controlStyle == 'thumbnails', - chevrons: this.controlStyle == 'chevrons', - button: this.controlStyle == 'chevrons', + thumbnails: this._controlConfig.style == 'thumbnails', + chevrons: this._controlConfig.style == 'chevrons', + button: this._controlConfig.style == 'chevrons', }; - if (this.controlStyle == 'chevrons') { + if (this._controlConfig.style == 'chevrons') { const icon = this.direction == 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right'; // TODO: Upon a safe distance from the release of HA 2021.11 these diff --git a/src/components/viewer.ts b/src/components/viewer.ts index feaf5f17..bc4724b8 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -19,7 +19,7 @@ import type { BrowseMediaSource, ExtendedHomeAssistant, MediaShowInfo, - NextPreviousControlStyle, + ViewerConfig, } from '../types.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; @@ -49,24 +49,15 @@ export class FrigateCardViewer extends LitElement { @property({ attribute: false }) protected view?: View; + @property({ attribute: false }) + protected viewerConfig?: ViewerConfig; + @property({ attribute: false }) protected browseMediaQueryParameters?: BrowseMediaQueryParameters; - @property({ attribute: false }) - protected nextPreviousControlStyle?: NextPreviousControlStyle; - - @property({ attribute: false }) - protected nextPreviousControlSize?: string; - - @property({ attribute: false }) - protected autoplayClip?: boolean; - @property({ attribute: false }) protected resolvedMediaCache?: ResolvedMediaCache; - @property({ attribute: false }) - protected lazyLoad?: boolean; - /** * Resolve all the given media for a target. * @param target The target to resolve media from. @@ -92,21 +83,23 @@ export class FrigateCardViewer extends LitElement { return errorFree; } + /** + * Master render method. + * @returns A rendered template. + */ protected render(): TemplateResult | void { return html`${until(this._render(), renderProgressIndicator())}`; } /** * Asyncronously render the element. - * @returns A template to render. + * @returns A rendered template. */ protected async _render(): Promise { if (!this.hass || !this.view || !this.browseMediaQueryParameters) { return html``; } - let autoplay = true; - if (this.view.is('clip') || this.view.is('snapshot')) { let parent: BrowseMediaSource | null = null; try { @@ -129,13 +122,6 @@ export class FrigateCardViewer extends LitElement { } this.view.target = parent; this.view.childIndex = childIndex; - - // In this block, no clip has been manually selected, so this is loading - // the most recent clip on card load. In this mode, autoplay of the clip - // may be disabled by configuration. If does not make sense to disable - // autoplay when the user has explicitly picked an event to play in the - // gallery. - autoplay = this.autoplayClip ?? true; } if (this.view.target && !(await this._resolveAllMediaForTarget(this.view.target))) { @@ -144,13 +130,10 @@ export class FrigateCardViewer extends LitElement { return html` `; } @@ -165,29 +148,20 @@ export class FrigateCardViewer extends LitElement { @customElement('frigate-card-viewer-core') export class FrigateCardViewerCore extends LitElement { + @property({ attribute: false }) + protected hass?: HomeAssistant & ExtendedHomeAssistant; + @property({ attribute: false }) protected view?: View; @property({ attribute: false }) - protected nextPreviousControlStyle?: NextPreviousControlStyle; - - @property({ attribute: false }) - protected nextPreviousControlSize?: string; - - @property({ attribute: false }) - protected resolvedMediaCache?: ResolvedMediaCache; - - @property({ attribute: false }) - protected autoplayClip?: boolean; - - @property({ attribute: false }) - protected hass?: HomeAssistant & ExtendedHomeAssistant; + protected viewerConfig?: ViewerConfig; @property({ attribute: false }) protected browseMediaQueryParameters?: BrowseMediaQueryParameters; @property({ attribute: false }) - protected lazyLoad?: boolean; + protected resolvedMediaCache?: ResolvedMediaCache; // Media carousel object. protected _carousel?: EmblaCarouselType; @@ -418,7 +392,7 @@ export class FrigateCardViewerCore extends LitElement { * Lazily load media in the carousel. */ protected _lazyLoadMediaHandler(): void { - if (!this.lazyLoad || !this._carousel) { + if (!this.viewerConfig?.lazy_load || !this._carousel) { return; } const slides = this._carousel.slideNodes(); @@ -504,8 +478,7 @@ export class FrigateCardViewerCore extends LitElement { ${neighbors && neighbors.previous ? html` this._nextPreviousHandler('previous')} @@ -519,8 +492,7 @@ export class FrigateCardViewerCore extends LitElement { ${neighbors && neighbors.next ? html` this._nextPreviousHandler('next')} @@ -594,7 +566,7 @@ export class FrigateCardViewerCore extends LitElement { ): TemplateResult | void { // media that can be expanded (folders) cannot be resolved to a single media // item, skip them. - if (!this.view || !BrowseMediaUtil.isTrueMedia(mediaToRender)) { + if (!this.view || !this.viewerConfig || !BrowseMediaUtil.isTrueMedia(mediaToRender)) { return; } @@ -603,20 +575,32 @@ export class FrigateCardViewerCore extends LitElement { return; } + // In this block, no clip has been manually selected, so this is loading + // the most recent clip on card load. In this mode, autoplay of the clip + // may be disabled by configuration. If does not make sense to disable + // autoplay when the user has explicitly picked an event to play in the + // gallery. + let autoplay = true; + if (this.view.is('clip') || this.view.is('snapshot')) { + autoplay = this.viewerConfig.autoplay_clip; + } + + const lazyLoad = this.viewerConfig.lazy_load; + return html`
${this.view.isClipRelatedView() ? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl' ? html`) => this._mediaShowEventHandler(slideIndex, e)} > @@ -626,7 +610,7 @@ export class FrigateCardViewerCore extends LitElement { muted controls playsinline - ?autoplay="${this.autoplayClip}" + ?autoplay="${autoplay}" @loadedmetadata="${(e: Event) => { this._mediaShowInfoHandler(slideIndex, createMediaShowInfo(e)); }}" @@ -634,14 +618,14 @@ export class FrigateCardViewerCore extends LitElement { @pause=${() => dispatchPauseEvent(this)} > ` : html` { if (this._carousel?.clickAllowed()) { diff --git a/src/editor.ts b/src/editor.ts index c29d5f99..2491a116 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,10 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ + +// TODO Add editor for control sizes +// TODO reorganizse editor by section + import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { localize } from './localize/localize.js'; -import type { FrigateCardConfig } from './types.js'; +import { + FrigateCardConfig, + frigateCardConfigDefaults, + frigateCardConfigSchema, +} from './types.js'; import frigate_card_editor_style from './scss/editor.scss'; @@ -84,66 +92,70 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor return html``; } - // The climate more-info has ha-switch and paper-dropdown-menu elements that are lazy loaded unless explicitly done here + // The climate more-info has ha-switch and paper-dropdown-menu elements that + // are lazy loaded unless explicitly loaded via climate here. this._helpers.importMoreInfoControl('climate'); - // You can restrict on domain type const cameraEntities = this._getEntities('camera'); const webrtcCameraEntity = - this._config?.webrtc && (this._config?.webrtc as any).entity - ? (this._config?.webrtc as any).entity + this._config?.live?.webrtc && (this._config?.live.webrtc as any).entity + ? (this._config?.live.webrtc as any).entity : ''; const viewModes = { '': '', - live: localize('menu.live'), - clips: localize('menu.clips'), - snapshots: localize('menu.snapshots'), - clip: localize('menu.clip'), - snapshot: localize('menu.snapshot'), - image: localize('menu.image'), + live: localize('config.view.views.live'), + clips: localize('config.view.views.clips'), + snapshots: localize('config.view.views.snapshots'), + clip: localize('config.view.views.clip'), + snapshot: localize('config.view.views.snapshot'), + image: localize('config.view.views.image'), }; const menuModes = { '': '', - none: localize('menu_mode.none'), - 'hidden-top': localize('menu_mode.hidden-top'), - 'hidden-left': localize('menu_mode.hidden-left'), - 'hidden-bottom': localize('menu_mode.hidden-bottom'), - 'hidden-right': localize('menu_mode.hidden-right'), - 'overlay-top': localize('menu_mode.overlay-top'), - 'overlay-left': localize('menu_mode.overlay-left'), - 'overlay-bottom': localize('menu_mode.overlay-bottom'), - 'overlay-right': localize('menu_mode.overlay-right'), - 'hover-top': localize('menu_mode.hover-top'), - 'hover-left': localize('menu_mode.hover-left'), - 'hover-bottom': localize('menu_mode.hover-bottom'), - 'hover-right': localize('menu_mode.hover-right'), - above: localize('menu_mode.above'), - below: localize('menu_mode.below'), + none: localize('config.menu.modes.none'), + 'hidden-top': localize('config.menu.modes.hidden-top'), + 'hidden-left': localize('config.menu.modes.hidden-left'), + 'hidden-bottom': localize('config.menu.modes.hidden-bottom'), + 'hidden-right': localize('config.menu.modes.hidden-right'), + 'overlay-top': localize('config.menu.modes.overlay-top'), + 'overlay-left': localize('config.menu.modes.overlay-left'), + 'overlay-bottom': localize('config.menu.modes.overlay-bottom'), + 'overlay-right': localize('config.menu.modes.overlay-right'), + 'hover-top': localize('config.menu.modes.hover-top'), + 'hover-left': localize('config.menu.modes.hover-left'), + 'hover-bottom': localize('config.menu.modes.hover-bottom'), + 'hover-right': localize('config.menu.modes.hover-right'), + above: localize('config.menu.modes.above'), + below: localize('config.menu.modes.below'), }; - const liveProvider = { + const liveProviders = { '': '', - frigate: localize('live_provider.frigate'), - 'frigate-jsmpeg': localize('live_provider.frigate-jsmpeg'), - webrtc: localize('live_provider.webrtc'), + frigate: localize('config.live.providers.frigate'), + 'frigate-jsmpeg': localize('config.live.providers.frigate-jsmpeg'), + webrtc: localize('config.live.providers.webrtc'), }; - const controlsNextPrev = { + const eventViewerNextPreviousControlStyles = { '': '', - thumbnails: localize('control.thumbnails'), - chevrons: localize('control.chevrons'), - none: localize('control.none'), + thumbnails: localize( + 'config.event_viewer.controls.next_previous.styles.thumbnails', + ), + chevrons: localize('config.event_viewer.controls.next_previous.styles.chevrons'), + none: localize('config.event_viewer.controls.next_previous.styles.none'), }; const aspectRatioModes = { '': '', - dynamic: localize('aspect_ratio_mode.dynamic'), - static: localize('aspect_ratio_mode.static'), - unconstrained: localize('aspect_ratio_mode.unconstrained'), - } + dynamic: localize('config.dimensions.aspect_ratio_modes.dynamic'), + static: localize('config.dimensions.aspect_ratio_modes.static'), + unconstrained: localize('config.dimensions.aspect_ratio_modes.unconstrained'), + }; + + const defaults = frigateCardConfigDefaults; return html`
@@ -158,7 +170,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ? html`
@@ -174,29 +186,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor - - ${Object.keys(viewModes).map((key) => { - return html` - ${viewModes[key]} - `; - })} - - + + ${Object.keys(viewModes).map((key) => { + return html` + ${viewModes[key]} + `; + })} + +
` : ''} @@ -210,62 +222,64 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${options.optional.show ? html`
- ${Object.keys(liveProvider).map((key) => { + ${Object.keys(liveProviders).map((key) => { return html` - ${liveProvider[key]} + ${liveProviders[key]} `; })} - + - + - + @@ -281,14 +295,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.appearance.show ? html` ${Object.keys(menuModes).map((key) => { @@ -300,26 +314,28 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
- ${Object.keys(controlsNextPrev).map((key) => { + ${Object.keys(eventViewerNextPreviousControlStyles).map((key) => { return html` - ${controlsNextPrev[key]} + + ${eventViewerNextPreviousControlStyles[key]} + `; })}
@@ -338,7 +354,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
- -
- - - -
- - @@ -390,11 +387,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor @@ -402,11 +400,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor @@ -414,11 +413,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor @@ -426,11 +426,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor @@ -438,11 +439,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor + +
+ + + +
+ + @@ -458,17 +486,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${options.advanced.show ? html`
` @@ -483,9 +511,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${options.webrtc.show ? html`
` : ''} @@ -549,13 +577,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor let objectTarget = newConfig; if (key.includes('.')) { - const parts = key.split('.', 2); - const configName = parts[0]; - if (!(configName in newConfig)) { - newConfig[configName] = {}; - } - objectTarget = newConfig[configName]; - key = parts[1]; + const parts = key.split('.'); + objectTarget = parts.slice(0, -1).reduce((obj, key) => { + if (!(key in obj)) { + obj[key] = {}; + } + return obj[key]; + }, newConfig); + key = parts[parts.length-1]; } if (value !== undefined && objectTarget[key] === value) { diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index f2b46c1b..7a45a692 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -1,15 +1,103 @@ { "common": { "version": "Version", - "show_warning": "Show Warning", - "show_error": "Show Error", - "frigate_card": "Frigate Card", + "show_warning": "Show warning", + "show_error": "Show error", + "frigate_card": "Frigate card", "frigate_card_description": "A Lovelace card for use with Frigate", "no_snapshots": "No snapshots", "no_clips": "No clips", "no_snapshot": "No recent snapshot", "no_clip": "No recent clip" }, + "config": { + "camera_entity": "Camera Entity", + "frigate": { + "camera_name": "Frigate camera name (Optional, autodetected from entity)", + "client_id": "Frigate client id (For >1 Frigate server)", + "label": "Label/object filter", + "url": "Frigate server URL", + "zone": "Zone" + }, + "view": { + "default": "Default view", + "views": { + "live": "Live view", + "clip": "Most recent clip", + "clips": "Clips gallery", + "snapshot": "Most recent snapshot", + "snapshots": "Snapshots gallery", + "image": "Static image" + }, + "timeout": "View timeout (seconds)" + }, + "event_viewer": { + "autoplay_clip": "Autoplay most recent clip", + "lazy_load": "Lazily load event media", + "controls": { + "next_previous": { + "style": "Event Viewer Next & Previous Control style", + "size": "Event Viewer Next & Previous Control size", + "styles": { + "thumbnails": "Thumbnails", + "chevrons": "Chevrons", + "none": "None" + } + } + } + }, + "live": { + "preload": "Preload live view", + "provider": "Live view provider", + "providers": { + "frigate": "Frigate", + "frigate-jsmpeg": "Frigate JSMpeg", + "webrtc": "WebRTC" + }, + "webrtc": { + "entity": "WebRTC Camera Entity (Not a Frigate camera)", + "url": "WebRTC Camera URL" + } + }, + "image": { + "src": "Static image URL/data for image view" + }, + "menu": { + "buttons": { + "frigate": "Frigate menu / Default view", + "frigate_ui": "Frigate user Interface", + "fullscreen": "Fullscreen", + "download": "Download event media" + }, + "mode": "Menu mode", + "modes": { + "none": "No menu", + "hidden-top": "Hidden top", + "hidden-left": "Hidden left", + "hidden-bottom": "Hidden bottom", + "hidden-right": "Hidden right", + "overlay-top": "Overlay top", + "overlay-left": "Overlay left", + "overlay-bottom": "Overlay bottom", + "overlay-right": "Overlay right", + "hover-top": "Hover top", + "hover-left": "Hover left", + "hover-bottom": "Hover bottom", + "hover-right": "Hover right", + "above": "Above", + "below": "Below" + } + }, + "dimensions": { + "aspect_ratio": "Default aspect ratio", + "aspect_ratio_mode": "Aspect ratio mode", + "aspect_ratio_modes": { + "unconstrained": "Unconstrained aspect ratio", + "dynamic": "Aspect ratio adjusts to media", + "static": "Static aspect ratio" + } + } + }, "editor": { "basic": "Basic", "basic_secondary": "Basic options for most users", @@ -21,74 +109,7 @@ "webrtc_secondary": "Optional WebRTC parameters", "advanced": "Advanced", "advanced_secondary": "Advanced options", - "camera_entity": "Camera Entity (Optional)", - "frigate_camera_name": "Frigate camera name (Optional, autodetected from entity)", - "default_view": "Default view (Optional)", - "frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)", - "view_timeout": "View timeout (seconds)", - "frigate_url": "Frigate URL (Optional, for Frigate UI button)", - "autoplay_clip": "Autoplay latest clip", - "menu_mode": "Menu mode (Optional)", - "show_button": "Show button", - "zone": "Zone", - "label": "Frigate label/object filter (Optional)", - "live_provider": "Live view provider (Optional)", - "live_preload": "Preload live view", - "image": "Static image URL for image view (Optional)", - "lazy_load": "Lazily load event media" - }, - "menu": { - "frigate": "Frigate Menu / Default View", - "live": "Live View", - "clips": "Clips Gallery", - "snapshots": "Snapshots Gallery", - "clip": "Latest Clip", - "snapshot": "Latest Snapshot", - "frigate_ui": "Frigate User Interface", - "fullscreen": "Fullscreen", - "image": "Static Image", - "download": "Download event media" - }, - "control": { - "nextprev": "Media Next & Previous Controls", - "thumbnails": "Thumbnails", - "chevrons": "Chevrons", - "none": "None" - }, - "dimensions": { - "aspect_ratio_mode": "Aspect Ratio Mode", - "aspect_ratio": "Default aspect ratio (Optional)" - }, - "aspect_ratio_mode": { - "unconstrained": "Unconstrained aspect ratio", - "dynamic": "Aspect ratio adjusts to media", - "static": "Static aspect ratio" - }, - "menu_mode": { - "none": "No menu", - "hidden-top": "Hidden Top", - "hidden-left": "Hidden Left", - "hidden-bottom": "Hidden Bottom", - "hidden-right": "Hidden Right", - "overlay-top": "Overlay Top", - "overlay-left": "Overlay Left", - "overlay-bottom": "Overlay Bottom", - "overlay-right": "Overlay Right", - "hover-top": "Hover Top", - "hover-left": "Hover Left", - "hover-bottom": "Hover Bottom", - "hover-right": "Hover Right", - "above": "Above", - "below": "Below" - }, - "live_provider": { - "frigate": "Frigate", - "frigate-jsmpeg": "Frigate JSMpeg", - "webrtc": "WebRTC" - }, - "webrtc": { - "entity": "Optional WebRTC Camera Entity (Not a Frigate camera)", - "url": "Optional WebRTC Camera URL" + "show_button": "Show button" }, "error": { "empty_response": "Received empty response from Home Assistant for request", @@ -101,7 +122,7 @@ "invalid_configuration": "Invalid configuration", "invalid_configuration_no_hint": "No location hint available (bad or missing type?)", "missing_webrtc": "WebRTC component not found", - "no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate_camera_name", + "no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate.camera_name", "could_not_render_elements": "Could not render picture elements", "invalid_elements_config": "Invalid picture elements configuration", "jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path", diff --git a/src/scss/viewer.scss b/src/scss/viewer.scss index a0596030..9fec4d30 100644 --- a/src/scss/viewer.scss +++ b/src/scss/viewer.scss @@ -1,6 +1,11 @@ +:host { + --video-max-height: none; +} + ha-hls-player { transform-style: preserve-3d; } + img,video { width: 100%; height: 100%; diff --git a/src/types.ts b/src/types.ts index 42c5db02..6420751d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,7 +38,7 @@ export type FrigateCardView = | typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number] | typeof FRIGATE_CARD_VIEWS_INTERNAL[number]; -export const FRIGATE_MENU_MODES = [ +const FRIGATE_MENU_MODES = [ 'none', 'hidden-top', 'hidden-left', @@ -55,13 +55,8 @@ export const FRIGATE_MENU_MODES = [ 'above', 'below', ] as const; -export type FrigateMenuMode = typeof FRIGATE_MENU_MODES[number]; - export const NEXT_PREVIOUS_CONTROL_STYLES = ['none', 'thumbnails', 'chevrons'] as const; -export type NextPreviousControlStyle = typeof NEXT_PREVIOUS_CONTROL_STYLES[number]; - export const LIVE_PROVIDERS = ['frigate', 'frigate-jsmpeg', 'webrtc'] as const; -export type LiveProvider = typeof LIVE_PROVIDERS[number]; /** * Action Types (for "Picture Elements" / Menu) @@ -122,7 +117,7 @@ const elementsBaseSchema = z.object({ }); /** - * Picture Element Types + * Picture Element Configuration. * * All picture element types are validated (not just the Frigate card custom * ones) as a convenience to present the user with a consistent error display @@ -214,7 +209,7 @@ const customSchema = z if (!val.match(/^custom:(?!frigate-card).+/)) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "Frigate-card custom elements must match specific schemas", + message: 'Frigate-card custom elements must match specific schemas', fatal: true, }); } @@ -223,7 +218,7 @@ const customSchema = z .passthrough(); /** - * Custom Element Types + * Custom Element Types. */ export const menuIconSchema = iconSchema.merge( @@ -269,82 +264,199 @@ export type PictureElement = z.infer; const pictureElementsSchema = pictureElementSchema.array().optional(); export type PictureElements = z.infer; +/** + * Frigate configuration section. + */ +const frigateConfigDefault = { + client_id: 'frigate' as const, +}; +const frigateConfigDefaultSchema = z + .object({ + // No URL validation to allow relative URLs within HA (e.g. addons). + url: z.string().optional(), + client_id: z.string().optional().default(frigateConfigDefault.client_id), + camera_name: z.string().optional(), + label: z.string().optional(), + zone: z.string().optional(), + }) + .default(frigateConfigDefault); + +/** + * View configuration section. + */ +const viewConfigDefault = { + default: 'live' as const, + timeout: 180, +}; +const viewConfigSchema = z + .object({ + default: z + .enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED) + .optional() + .default(viewConfigDefault.default), + timeout: z + .number() + .or( + z + .string() + .regex(/^\d+$/) + .transform((val) => Number(val)), + ) + .optional() + .default(viewConfigDefault.timeout), + }) + .default(viewConfigDefault); + +/** + * Image view configuration section. + */ +const imageConfigSchema = z + .object({ + src: z.string().optional(), + }) + .optional(); +export type ImageViewConfig = z.infer; + +/** + * Live view configuration section. + */ +const liveConfigDefault = { + provider: 'frigate' as const, + preload: false, +}; +const webrtcConfigSchema = z + .object({ + entity: z.string().optional(), + url: z.string().optional(), + }) + .passthrough() + .optional(); +export type WebRTCConfig = z.infer; + +const liveConfigSchema = z + .object({ + provider: z.enum(LIVE_PROVIDERS).default(liveConfigDefault.provider), + preload: z.boolean().default(liveConfigDefault.preload), + webrtc: webrtcConfigSchema, + }) + .default(liveConfigDefault); + +/** + * Menu configuration section. + */ +const menuConfigDefault = { + mode: 'hidden-top' as const, + buttons: { + frigate: true, + live: true, + clips: true, + snapshots: true, + image: false, + download: true, + frigate_ui: true, + fullscreen: true, + }, + button_size: '40px', +}; +const menuConfigSchema = z + .object({ + mode: z.enum(FRIGATE_MENU_MODES).optional().default(menuConfigDefault.mode), + buttons: z + .object({ + frigate: z.boolean().default(menuConfigDefault.buttons.frigate), + live: z.boolean().default(menuConfigDefault.buttons.live), + clips: z.boolean().default(menuConfigDefault.buttons.clips), + snapshots: z.boolean().default(menuConfigDefault.buttons.snapshots), + image: z.boolean().default(menuConfigDefault.buttons.image), + download: z.boolean().default(menuConfigDefault.buttons.download), + frigate_ui: z.boolean().default(menuConfigDefault.buttons.frigate_ui), + fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen), + }) + .default(menuConfigDefault.buttons), + button_size: z.string().default(menuConfigDefault.button_size), + }) + .default(menuConfigDefault); +export type MenuConfig = z.infer; + +/** + * Event viewer configuration section (clip, snapshot, clip-specific, snapshot-specific). + */ +const viewerConfigDefault = { + autoplay_clip: false, + lazy_load: true, + controls: { + next_previous: { + size: '48px', + style: 'thumbnails' as const, + }, + }, +}; +const nextPreviousControlConfigSchema = z + .object({ + style: z + .enum(NEXT_PREVIOUS_CONTROL_STYLES) + .default(viewerConfigDefault.controls.next_previous.style), + size: z.string().default(viewerConfigDefault.controls.next_previous.size), + }) + .default(viewerConfigDefault.controls.next_previous); +export type NextPreviousControlConfig = z.infer; + +const viewerConfigSchema = z + .object({ + autoplay_clip: z.boolean().default(viewerConfigDefault.autoplay_clip), + lazy_load: z.boolean().default(viewerConfigDefault.lazy_load), + controls: z + .object({ + next_previous: nextPreviousControlConfigSchema, + }) + .default(viewerConfigDefault.controls), + }) + .default(viewerConfigDefault); +export type ViewerConfig = z.infer; + +/** + * Dimensions configuration section. + */ +const dimensionsConfigDefault = { + aspect_ratio_mode: 'dynamic' as const, + aspect_ratio: [16, 9], +}; +const dimensionsConfigSchema = z + .object({ + aspect_ratio_mode: z + .enum(['dynamic', 'static', 'unconstrained']) + .default(dimensionsConfigDefault.aspect_ratio_mode), + aspect_ratio: z + .number() + .array() + .length(2) + .or( + z + .string() + .regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/) + .transform((input) => input.split(/[:\/]/).map((d) => Number(d))), + ) + .default(dimensionsConfigDefault.aspect_ratio), + }) + .default(dimensionsConfigDefault); + +/** + * Main card config. + */ export const frigateCardConfigSchema = z.object({ camera_entity: z.string().optional(), - // No URL validation to allow relative URLs within HA (e.g. addons). - frigate_url: z.string().optional(), - frigate_client_id: z.string().optional().default('frigate'), - frigate_camera_name: z.string().optional(), - view_default: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED).optional().default('live'), - view_timeout: z - .number() - .or( - z - .string() - .regex(/^\d+$/) - .transform((val) => Number(val)), - ) - .optional() - .default(180), - live_provider: z.enum(LIVE_PROVIDERS).default('frigate'), - live_preload: z.boolean().default(false), - image: z.string().optional(), - webrtc: z - .object({ - entity: z.string().optional(), - url: z.string().optional(), - }) - .passthrough() - .optional(), - label: z.string().optional(), - zone: z.string().optional(), - autoplay_clip: z.boolean().default(false), - event_viewer: z - .object({ - lazy_load: z.boolean().default(true), - }) - .optional(), - menu_mode: z.enum(FRIGATE_MENU_MODES).optional().default('hidden-top'), - menu_buttons: z - .object({ - frigate: z.boolean().default(true), - live: z.boolean().default(true), - clips: z.boolean().default(true), - snapshots: z.boolean().default(true), - image: z.boolean().default(false), - download: z.boolean().default(true), - frigate_ui: z.boolean().default(true), - fullscreen: z.boolean().default(true), - }) - .optional(), - menu_button_size: z.string().default('40px'), - update_entities: z.string().array().optional(), + frigate: frigateConfigDefaultSchema, + view: viewConfigSchema, + menu: menuConfigSchema, + live: liveConfigSchema, + event_viewer: viewerConfigSchema, + image: imageConfigSchema, elements: pictureElementsSchema, - controls: z - .object({ - nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'), - nextprev_size: z.string().default('48px'), - }) - .optional(), - dimensions: z - .object({ - aspect_ratio_mode: z - .enum(['dynamic', 'static', 'unconstrained']) - .default('dynamic'), - aspect_ratio: z - .number() - .array() - .length(2) - .or( - z - .string() - .regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/) - .transform((input) => input.split(/[:\/]/).map((d) => Number(d))), - ) - .default([16, 9]), - }) - .optional(), + dimensions: dimensionsConfigSchema, + + // Entities that should trigger a card update. + update_entities: z.string().array().optional(), // Stock lovelace card config. type: z.string(), @@ -354,6 +466,14 @@ export const frigateCardConfigSchema = z.object({ }); export type FrigateCardConfig = z.infer; +export const frigateCardConfigDefaults = { + frigate: frigateConfigDefault, + view: viewConfigDefault, + menu: menuConfigDefault, + live: liveConfigDefault, + event_viewer: viewerConfigDefault, +}; + // Schema for card (non-user configured) menu icons. const internalMenuIconSchema = z.object({ type: z.literal('internal-menu-icon'), @@ -404,8 +524,8 @@ export interface Message { } export interface MenuInteraction { - interaction: "hold" | "tap" | "double_tap", - button: MenuButton, + interaction: 'hold' | 'tap' | 'double_tap'; + button: MenuButton; } /**