/* eslint-disable @typescript-eslint/no-explicit-any */ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { localize } from './localize/localize.js'; import { frigateCardConfigDefaults, RawFrigateCardConfig, RawFrigateCardConfigArray, } from './types.js'; import { CONF_CAMERAS, CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_CAMERA_NAME, CONF_CAMERAS_ARRAY_CLIENT_ID, CONF_CAMERAS_ARRAY_ICON, CONF_CAMERAS_ARRAY_ID, CONF_CAMERAS_ARRAY_LABEL, CONF_CAMERAS_ARRAY_LIVE_PROVIDER, CONF_CAMERAS_ARRAY_TITLE, CONF_CAMERAS_ARRAY_URL, CONF_CAMERAS_ARRAY_WEBRTC_ENTITY, CONF_CAMERAS_ARRAY_WEBRTC_URL, CONF_CAMERAS_ARRAY_ZONE, CONF_DIMENSIONS_ASPECT_RATIO, CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_EVENT_GALLERY_MIN_COLUMNS, CONF_EVENT_VIEWER_AUTOPLAY_CLIP, CONF_EVENT_VIEWER_AUTO_UNMUTE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE, CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_EVENT_VIEWER_CONTROLS_TITLE_DURATION_SECONDS, CONF_EVENT_VIEWER_CONTROLS_TITLE_MODE, CONF_EVENT_VIEWER_DRAGGABLE, CONF_EVENT_VIEWER_LAZY_LOAD, CONF_IMAGE_REFRESH_SECONDS, CONF_IMAGE_SRC, CONF_LIVE_AUTO_UNMUTE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, CONF_LIVE_CONTROLS_THUMBNAILS_MODE, CONF_LIVE_CONTROLS_THUMBNAILS_SIZE, CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS, CONF_LIVE_CONTROLS_TITLE_MODE, CONF_LIVE_DRAGGABLE, CONF_LIVE_LAZY_LOAD, CONF_LIVE_LAZY_UNLOAD, CONF_LIVE_PRELOAD, CONF_MENU_BUTTONS_CLIPS, CONF_MENU_BUTTONS_FRIGATE, CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD, CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN, CONF_MENU_BUTTONS_FRIGATE_UI, CONF_MENU_BUTTONS_IMAGE, CONF_MENU_BUTTONS_LIVE, CONF_MENU_BUTTONS_SNAPSHOTS, CONF_MENU_BUTTON_SIZE, CONF_MENU_MODE, CONF_VIEW_CAMERA_SELECT, CONF_VIEW_DEFAULT, CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_UPDATE_CYCLE_CAMERA, CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, } from './const.js'; import { arrayMove, getEntityTitle, prettifyFrigateName } from './common.js'; import { copyConfig, deleteConfigValue, getArrayConfigPath, getConfigValue, isConfigUpgradeable, setConfigValue, upgradeConfig, } from './config-mgmt.js'; import frigate_card_editor_style from './scss/editor.scss'; interface EditorOptionsSet { icon: string; name: string; secondary: string; show: boolean; } interface EditorOptions { [setName: string]: EditorOptionsSet; } interface ConfigValueTarget { configValue: string; checked?: boolean; value?: string; } interface EditorCameraTarget { cameraIndex: number; } interface EditorOptionSetTarget { optionSetName: string; } const options: EditorOptions = { cameras: { icon: 'video', name: localize('editor.cameras'), secondary: localize('editor.cameras_secondary'), show: true, }, view: { icon: 'eye', name: localize('editor.view'), secondary: localize('editor.view_secondary'), show: false, }, menu: { icon: 'menu', name: localize('editor.menu'), secondary: localize('editor.menu_secondary'), show: false, }, live: { icon: 'cctv', name: localize('editor.live'), secondary: localize('editor.live_secondary'), show: false, }, event_viewer: { icon: 'filmstrip', name: localize('editor.event_viewer'), secondary: localize('editor.event_viewer_secondary'), show: false, }, event_gallery: { icon: 'grid', name: localize('editor.event_gallery'), secondary: localize('editor.event_gallery_secondary'), show: false, }, image: { icon: 'image', name: localize('editor.image'), secondary: localize('editor.image_secondary'), show: false, }, dimensions: { icon: 'aspect-ratio', name: localize('editor.dimensions'), secondary: localize('editor.dimensions_secondary'), show: false, }, }; @customElement('frigate-card-editor') export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { @property({ attribute: false }) public hass?: HomeAssistant; @state() protected _config?: RawFrigateCardConfig; @state() protected _helpers?: any; protected _initialized = false; protected _configUpgradeable = false; @property({ attribute: false }) protected _expandedCameraIndex: number | null = null; protected _viewModes = new Map([ ['', ''], ['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')], ]); protected _cameraSelectViewModes = new Map([ ...this._viewModes, ['current', localize('config.view.views.current')], ]); protected _menuModes = new Map([ ['', ''], ['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')], ]); protected _eventViewerNextPreviousControlStyles = new Map([ ['', ''], [ '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')], ]); protected _liveNextPreviousControlStyles = new Map([ ['', ''], ['chevrons', localize('config.live.controls.next_previous.styles.chevrons')], ['icons', localize('config.live.controls.next_previous.styles.icons')], ['none', localize('config.live.controls.next_previous.styles.none')], ]); protected _aspectRatioModes = new Map([ ['', ''], ['dynamic', localize('config.dimensions.aspect_ratio_modes.dynamic')], ['static', localize('config.dimensions.aspect_ratio_modes.static')], ['unconstrained', localize('config.dimensions.aspect_ratio_modes.unconstrained')], ]); protected _thumbnailModes = new Map([ ['', ''], ['none', localize('config.event_viewer.controls.thumbnails.modes.none')], ['above', localize('config.event_viewer.controls.thumbnails.modes.above')], ['below', localize('config.event_viewer.controls.thumbnails.modes.below')], ]); protected _thumbnailMedias = new Map([ ['', ''], ['clips', localize('config.live.controls.thumbnails.medias.clips')], ['snapshots', localize('config.live.controls.thumbnails.medias.snapshots')], ]); protected _titleModes = new Map([ ['', ''], ['none', localize('config.event_viewer.controls.title.modes.none')], [ 'popup-top-left', localize('config.event_viewer.controls.title.modes.popup-top-left'), ], [ 'popup-top-right', localize('config.event_viewer.controls.title.modes.popup-top-right'), ], [ 'popup-bottom-left', localize('config.event_viewer.controls.title.modes.popup-bottom-left'), ], [ 'popup-bottom-right', localize('config.event_viewer.controls.title.modes.popup-bottom-right'), ], ]); public setConfig(config: RawFrigateCardConfig): void { // Note: This does not use Zod to parse the configuration, so it may be // partially or completely invalid. It's more useful to have a partially // valid configuration here, to allow the user to fix the broken parts. As // such, RawFrigateCardConfig is used as the type. this._config = config; this._configUpgradeable = isConfigUpgradeable(config); this.loadCardHelpers(); } protected shouldUpdate(): boolean { if (!this._initialized) { this._initialize(); } return true; } protected _getEntities(domain: string): string[] { if (!this.hass) { return []; } const entities = Object.keys(this.hass.states).filter( (eid) => eid.substr(0, eid.indexOf('.')) === domain, ); entities.sort(); // Add a blank entry to unset a selection. entities.unshift(''); return entities; } /** * Render an option set header * @param optionSetName The name of the EditorOptionsSet. * @returns A rendered template. */ protected _renderOptionSetHeader(optionSetName: string): TemplateResult { const optionSet = options[optionSetName]; return html`
${optionSet.name}
${optionSet.secondary}
`; } /** * Get a localized help label for a given config path. * @param configPath The config path. * @returns A localized label. */ protected _getLabel(configPath: string): string { // Strip out array indices from the path. const path = configPath .split('.') .filter((e) => !e.match(/^\[[0-9]+\]$/)) .join('.'); return localize(`config.${path}`); } /** * Render a dropdown menu. * @param configPath The configuration path to set/read. * @param dropdown The downdown in an array or key/value dictionary. * @returns A rendered template. */ protected _renderDropdown( configPath: string, dropdown: string[] | Map, ): TemplateResult | void { if (!this._config) { return; } const keys = Array.isArray(dropdown) ? dropdown : [...dropdown.keys()]; return html` ${keys.map( (key) => html` ${Array.isArray(dropdown) ? key : dropdown.get(key)} `, )} `; } protected _renderSlider( configPath: string, valueDefault: number, icon: string, min: number, max: number, ): TemplateResult | void { if (!this._config) { return; } const value = Number(getConfigValue(this._config, configPath, valueDefault)); return html` `; } /** * Render a camera header. * @param cameraIndex The index of the camera to edit/add. * @param cameraConfig The configuration of the camera in question. * @param addNewCamera Whether or not this is a header to add a new camera. * @returns A rendered template. */ protected _renderCameraHeader( cameraIndex: number, cameraConfig?: RawFrigateCardConfig, addNewCamera?: boolean, ): TemplateResult { return html`
${addNewCamera ? html` [${localize('editor.add_new_camera')}...] ` : // Attempt to render a recognizable name for the camera, // starting with the most likely to be useful and working our // ways towards the least useful. html` ${cameraConfig?.title || cameraConfig?.id || [ cameraConfig?.camera_entity ? getEntityTitle(this.hass, String(cameraConfig.camera_entity)) : '', cameraConfig?.client_id, cameraConfig?.camera_name ? prettifyFrigateName(String(cameraConfig.camera_name)) : '', cameraConfig?.label ? prettifyFrigateName(String(cameraConfig.label)) : '', cameraConfig?.zone ? prettifyFrigateName(String(cameraConfig.zone)) : '', ] .filter(Boolean) .join(' / ') || localize('editor.camera') + ' #' + cameraIndex} `}
`; } /** * Render a camera section. * @param cameras The full array of cameras. * @param cameraIndex The index (in the array) to render. * @param cameraEntities The full list of camera entities. * @param addNewCamera Whether or not this is a section to add a new non-existent camera. * @returns A rendered template. */ protected _renderCamera( cameras: RawFrigateCardConfigArray, cameraIndex: number, cameraEntities: string[], addNewCamera?: boolean, ): TemplateResult | void { const liveProviders = new Map([ ['', ''], ['auto', localize('config.cameras.live_providers.auto')], ['frigate', localize('config.cameras.live_providers.frigate')], ['frigate-jsmpeg', localize('config.cameras.live_providers.frigate-jsmpeg')], ['webrtc', localize('config.cameras.live_providers.webrtc')], ]); // Make a new config and update the editor with changes on it, const modifyConfig = (func: (config: RawFrigateCardConfig) => boolean): void => { if (this._config) { const newConfig = copyConfig(this._config); if (func(newConfig)) { this._updateConfig(newConfig); } } }; return html` ${this._renderCameraHeader(cameraIndex, cameras[cameraIndex], addNewCamera)} ${this._expandedCameraIndex === cameraIndex ? html`
!addNewCamera && modifyConfig((config: RawFrigateCardConfig): boolean => { if (Array.isArray(config.cameras) && cameraIndex > 0) { arrayMove(config.cameras, cameraIndex, cameraIndex - 1); this._expandedCameraIndex = cameraIndex - 1; return true; } return false; })} > = this._config.cameras.length - 1} @click=${() => !addNewCamera && modifyConfig((config: RawFrigateCardConfig): boolean => { if ( Array.isArray(config.cameras) && cameraIndex < config.cameras.length - 1 ) { arrayMove(config.cameras, cameraIndex, cameraIndex + 1); this._expandedCameraIndex = cameraIndex + 1; return true; } return false; })} > { modifyConfig((config: RawFrigateCardConfig): boolean => { if (Array.isArray(config.cameras)) { config.cameras.splice(cameraIndex, 1); this._expandedCameraIndex = null; return true; } return false; }); }} >
${this._renderDropdown( getArrayConfigPath(CONF_CAMERAS_ARRAY_CAMERA_ENTITY, cameraIndex), cameraEntities, )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_CAMERA_NAME, cameraIndex), )} ${this._renderDropdown( getArrayConfigPath(CONF_CAMERAS_ARRAY_LIVE_PROVIDER, cameraIndex), liveProviders, )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_URL, cameraIndex), )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_LABEL, cameraIndex), )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_ZONE, cameraIndex), )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_CLIENT_ID, cameraIndex), )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_TITLE, cameraIndex), )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_ICON, cameraIndex), )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex), )} ${this._renderDropdown( getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_ENTITY, cameraIndex), cameraEntities, )} ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_URL, cameraIndex), )}
` : ``} `; } /** * Render a string input field. * @param configPath The configuration path to set/read. * @param allowedPattern An allowed input pattern. * @returns A rendered template. */ protected _renderStringInput( configPath: string, type?: 'text' | 'number', ): TemplateResult | void { if (!this._config) { return; } const allowedPattern = type == 'number' ? '[0-9]' : undefined; return html` `; } /** * Render a switch. * @param configPath The configuration path to set/read. * @param valueDefault The default switch value if unset. * @param label An optional switch label. * @returns A rendered template. */ protected _renderSwitch( configPath: string, valueDefault: boolean, label?: string, ): TemplateResult | void { if (!this._config) { return; } return html` `; } protected _updateConfig(config: RawFrigateCardConfig): void { this._config = config; fireEvent(this, 'config-changed', { config: this._config }); } protected render(): TemplateResult | void { if (!this.hass || !this._helpers || !this._config) { return html``; } // The climate more-info loads ha-switch and paper-dropdown-menu. this._helpers.importMoreInfoControl('climate'); // The light more-info loads ha-labeled-slider. this._helpers.importMoreInfoControl('light'); const cameraEntities = this._getEntities('camera'); const defaults = frigateCardConfigDefaults; const getShowButtonLabel = (configPath: string) => localize('editor.show_button') + ': ' + localize(`config.${configPath}`); const cameras = (getConfigValue(this._config, CONF_CAMERAS) || []) as RawFrigateCardConfigArray; return html` ${this._configUpgradeable ? html`
${localize('editor.upgrade_available')} { if (this._config) { const upgradedConfig = copyConfig(this._config); upgradeConfig(upgradedConfig); this._updateConfig(upgradedConfig); } }} >

` : html``}
${this._renderOptionSetHeader('cameras')} ${options.cameras.show ? html`
${cameras.map((_, index) => this._renderCamera(cameras, index, cameraEntities), )} ${this._renderCamera(cameras, cameras.length, cameraEntities, true)}
` : ''} ${this._renderOptionSetHeader('view')} ${options.view.show ? html`
${this._renderDropdown(CONF_VIEW_DEFAULT, this._viewModes)} ${this._renderDropdown( CONF_VIEW_CAMERA_SELECT, this._cameraSelectViewModes, )} ${this._renderStringInput(CONF_VIEW_TIMEOUT_SECONDS, 'number')} ${this._renderStringInput(CONF_VIEW_UPDATE_SECONDS, 'number')} ${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)} ${this._renderSwitch( CONF_VIEW_UPDATE_CYCLE_CAMERA, defaults.view.update_cycle_camera, )}
` : ''} ${this._renderOptionSetHeader('menu')} ${options.menu.show ? html`
${this._renderDropdown(CONF_MENU_MODE, this._menuModes)} ${this._renderStringInput(CONF_MENU_BUTTON_SIZE)} ${this._renderSwitch( CONF_MENU_BUTTONS_FRIGATE, defaults.menu.buttons.frigate, getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE), )} ${this._renderSwitch( CONF_MENU_BUTTONS_LIVE, defaults.menu.buttons.live, getShowButtonLabel('view.views.live'), )} ${this._renderSwitch( CONF_MENU_BUTTONS_CLIPS, defaults.menu.buttons.clips, getShowButtonLabel('view.views.clips'), )} ${this._renderSwitch( CONF_MENU_BUTTONS_SNAPSHOTS, defaults.menu.buttons.snapshots, getShowButtonLabel('view.views.snapshots'), )} ${this._renderSwitch( CONF_MENU_BUTTONS_IMAGE, defaults.menu.buttons.image, getShowButtonLabel('view.views.image'), )} ${this._renderSwitch( CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD, defaults.menu.buttons.download, getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD), )} ${this._renderSwitch( CONF_MENU_BUTTONS_FRIGATE_UI, defaults.menu.buttons.frigate_ui, getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_UI), )} ${this._renderSwitch( CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN, defaults.menu.buttons.fullscreen, getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN), )}
` : ''} ${this._renderOptionSetHeader('live')} ${options.live.show ? html`
${this._renderSwitch(CONF_LIVE_PRELOAD, defaults.live.preload)} ${this._renderSwitch(CONF_LIVE_DRAGGABLE, defaults.live.draggable)} ${this._renderSwitch(CONF_LIVE_LAZY_LOAD, defaults.live.lazy_load)} ${this._renderSwitch(CONF_LIVE_LAZY_UNLOAD, defaults.live.lazy_unload)} ${this._renderSwitch(CONF_LIVE_AUTO_UNMUTE, defaults.live.auto_unmute)} ${this._renderDropdown( CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, this._liveNextPreviousControlStyles, )} ${this._renderStringInput(CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE)} ${this._renderDropdown( CONF_LIVE_CONTROLS_THUMBNAILS_MODE, this._thumbnailModes, )} ${this._renderDropdown( CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, this._thumbnailMedias, )} ${this._renderStringInput(CONF_LIVE_CONTROLS_THUMBNAILS_SIZE)} ${this._renderDropdown(CONF_LIVE_CONTROLS_TITLE_MODE, this._titleModes)} ${this._renderStringInput( CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS, 'number', )}
` : ''} ${this._renderOptionSetHeader('event_gallery')} ${options.event_gallery.show ? html`
${this._renderSlider( CONF_EVENT_GALLERY_MIN_COLUMNS, defaults.event_gallery.min_columns, 'mdi:view-column', 1, 10, )}
` : ''} ${this._renderOptionSetHeader('event_viewer')} ${options.event_viewer.show ? html`
${this._renderSwitch( CONF_EVENT_VIEWER_AUTOPLAY_CLIP, defaults.event_viewer.autoplay_clip, )} ${this._renderSwitch( CONF_EVENT_VIEWER_AUTO_UNMUTE, defaults.event_viewer.auto_unmute, )} ${this._renderSwitch( CONF_EVENT_VIEWER_DRAGGABLE, defaults.event_viewer.draggable, )} ${this._renderSwitch( CONF_EVENT_VIEWER_LAZY_LOAD, defaults.event_viewer.lazy_load, )} ${this._renderDropdown( CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, this._eventViewerNextPreviousControlStyles, )} ${this._renderStringInput(CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE)} ${this._renderDropdown( CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE, this._thumbnailModes, )} ${this._renderStringInput(CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE)} ${this._renderDropdown( CONF_EVENT_VIEWER_CONTROLS_TITLE_MODE, this._titleModes, )} ${this._renderStringInput( CONF_EVENT_VIEWER_CONTROLS_TITLE_DURATION_SECONDS, 'number', )}
` : ''} ${this._renderOptionSetHeader('image')} ${options.image.show ? html`
${this._renderStringInput(CONF_IMAGE_SRC)} ${this._renderStringInput(CONF_IMAGE_REFRESH_SECONDS, 'number')}
` : ''} ${this._renderOptionSetHeader('dimensions')} ${options.dimensions.show ? html`
${this._renderDropdown( CONF_DIMENSIONS_ASPECT_RATIO_MODE, this._aspectRatioModes, )} ${this._renderStringInput(CONF_DIMENSIONS_ASPECT_RATIO)}
` : ''}
`; } /** * Verify editor is initialized. */ protected _initialize(): void { if (this.hass === undefined) return; if (this._config === undefined) return; if (this._helpers === undefined) return; this._initialized = true; } /** * Load card helpers. */ protected async loadCardHelpers(): Promise { this._helpers = await (window as any).loadCardHelpers(); } /** * Display/hide a camera section. * @param ev The event triggering the change. */ protected _toggleCameraHandler(ev: { target: EditorCameraTarget | null }): void { if (ev && ev.target) { this._expandedCameraIndex = this._expandedCameraIndex == ev.target.cameraIndex ? null : ev.target.cameraIndex; } } /** * Handle a toggled set of options. * @param ev The event triggering the change. */ protected _toggleOptionHandler(ev: { target: EditorOptionSetTarget | null }): void { this._toggleOptionSet(ev, options); } /** * Toggle display of a set of options (e.g. 'Live') * @param ev The event triggering the change. * @param options The EditorOptions object. */ protected _toggleOptionSet( ev: { target: EditorOptionSetTarget | null }, options: EditorOptions, ): void { if (ev && ev.target) { const show = !options[ev.target.optionSetName].show; for (const [key] of Object.entries(options)) { options[key].show = false; } options[ev.target.optionSetName].show = show; this.requestUpdate(); } } /** * Handle a changed option value. * @param ev Event triggering the change. */ protected _valueChangedHandler(ev: { target: (ConfigValueTarget & HTMLElement) | null; }): void { const target = ev.target; if (!this._config || !this.hass || !target) { return; } let value; if ('checked' in target) { value = target.checked; } else if (typeof target.value === 'string') { value = target.value?.trim(); if (target['type'] === 'number' && value != '') { value = Number(value); } } else { value = target.value; } const key: string = target.configValue; if (!key) { return; } if (getConfigValue(this._config, key) === value) { return; } const newConfig = copyConfig(this._config); if (value === '' || typeof value === 'undefined') { deleteConfigValue(newConfig, key); } else { setConfigValue(newConfig, key, value); } this._updateConfig(newConfig); } /** * Return compiled CSS styles. */ static get styles(): CSSResultGroup { return unsafeCSS(frigate_card_editor_style); } }