Basic configuration upgrade support.
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
"@material/image-list": "^12.0.0",
|
||||
"custom-card-helpers": "^1.8.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"dlv": "github:developit/dlv",
|
||||
"dset": "^3.1.1",
|
||||
"embla-carousel": "^5.0.1",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"lit": "^2.0.2",
|
||||
|
||||
+11
-4
@@ -20,7 +20,7 @@ import {
|
||||
import screenfull from 'screenfull';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { entitySchema, frigateCardConfigSchema, MenuInteraction } from './types.js';
|
||||
import { entitySchema, frigateCardConfigSchema, MenuInteraction, RawFrigateCardConfig } from './types.js';
|
||||
import type {
|
||||
BrowseMediaQueryParameters,
|
||||
Entity,
|
||||
@@ -58,6 +58,7 @@ import './patches/ha-hls-player.js';
|
||||
import cardStyle from './scss/card.scss';
|
||||
import { ResolvedMediaCache } from './resolved-media.js';
|
||||
import { BrowseMediaUtil } from './browse-media-util.js';
|
||||
import { isConfigUpgradeable } from './config-mgmt.js';
|
||||
|
||||
/** A note on media callbacks:
|
||||
*
|
||||
@@ -401,19 +402,25 @@ export class FrigateCard extends LitElement {
|
||||
* Set the card configuration.
|
||||
* @param inputConfig The card configuration.
|
||||
*/
|
||||
public setConfig(inputConfig: FrigateCardConfig): void {
|
||||
public setConfig(inputConfig: RawFrigateCardConfig): void {
|
||||
if (!inputConfig) {
|
||||
throw new Error(localize('error.invalid_configuration:'));
|
||||
throw new Error(localize('error.invalid_configuration'));
|
||||
}
|
||||
|
||||
const configUpgradeable = isConfigUpgradeable(inputConfig);
|
||||
const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
|
||||
if (!parseResult.success) {
|
||||
const hint = this._getParseErrorPaths(parseResult.error);
|
||||
let upgradeMessage = '';
|
||||
if (configUpgradeable && getLovelace().mode !== 'yaml') {
|
||||
upgradeMessage = `${localize('editor.upgrade_available_in_editor')}. `;
|
||||
}
|
||||
throw new Error(
|
||||
upgradeMessage +
|
||||
`${localize('error.invalid_configuration')}: ` +
|
||||
(hint.length
|
||||
? JSON.stringify(hint, null, ' ')
|
||||
: localize('error.invalid_configuration_no_hint')),
|
||||
: localize('error.invalid_configuration_no_hint'))
|
||||
);
|
||||
}
|
||||
const config = parseResult.data;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import delve from 'dlv';
|
||||
import { dset } from 'dset';
|
||||
import { RawFrigateCardConfig } from './types';
|
||||
|
||||
/**
|
||||
* Set a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param key The key to the property to set.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
export const setConfigValue = (
|
||||
obj: RawFrigateCardConfig,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
dset(obj, key, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param key The key to the property to retrieve.
|
||||
* @returns The property or undefined if not found.
|
||||
*/
|
||||
export const getConfigValue = (obj: RawFrigateCardConfig, key: string, def?: unknown): unknown => {
|
||||
return delve(obj, key, def);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a configuration value.
|
||||
* @param obj The configuration.
|
||||
* @param key The key to the property to delete.
|
||||
*/
|
||||
export const deleteConfigValue = (obj: RawFrigateCardConfig, key: string): void => {
|
||||
let id = key;
|
||||
let targetObj: unknown = obj;
|
||||
if (key && key.split && key.includes('.')) {
|
||||
const keys = key.split('.');
|
||||
id = keys[keys.length - 1];
|
||||
targetObj = getConfigValue(obj, keys.slice(0, -1).join('.'));
|
||||
}
|
||||
if (targetObj && typeof targetObj === 'object') {
|
||||
delete targetObj[id];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upgrade a configuration.
|
||||
* @param obj The configuration to upgrade.
|
||||
* @returns `true` if the configuration is modified.
|
||||
*/
|
||||
export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
|
||||
let upgraded = false;
|
||||
for (let i = 0; i < UPGRADES.length; i++) {
|
||||
upgraded = UPGRADES[i](obj) || upgraded;
|
||||
}
|
||||
trimConfig(obj);
|
||||
return upgraded;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if a configuration is automatically upgradeable.
|
||||
* @param obj The configuration. It is not modified.
|
||||
* @returns `true` if the configuration is upgradeable.
|
||||
*/
|
||||
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
|
||||
const newObj = JSON.parse(JSON.stringify(obj));
|
||||
return upgradeConfig(newObj);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove empty sections from a configuration.
|
||||
* @param obj Configuration object.
|
||||
*/
|
||||
export const trimConfig = function (obj: RawFrigateCardConfig): void {
|
||||
const keys = Object.keys(obj);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (typeof obj[key] === 'object' && obj[key] != null) {
|
||||
trimConfig(obj[key] as RawFrigateCardConfig);
|
||||
|
||||
if (!Object.keys(obj[key] as RawFrigateCardConfig).length) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy a configuration.
|
||||
* @param obj Configuration to copy.
|
||||
* @returns A new deeply-copied configuration.
|
||||
*/
|
||||
export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardConfig {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a property is not an object.
|
||||
* @param value The property.
|
||||
* @returns `true` is the value is not an object.
|
||||
*/
|
||||
const isNotObject = function (value: unknown) {
|
||||
return typeof value !== 'object' ? value : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a property from one location to another.
|
||||
* @param oldPath The old property path.
|
||||
* @param newPath The new property path.
|
||||
* @param transform An optional transform for the value.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
const upgradeMoveTo = function (
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
transform?: (valueIn: unknown) => unknown,
|
||||
): (obj: RawFrigateCardConfig) => boolean {
|
||||
return function (obj: RawFrigateCardConfig): boolean {
|
||||
let value = getConfigValue(obj, oldPath);
|
||||
if (transform) {
|
||||
value = transform(value);
|
||||
}
|
||||
if (typeof value !== 'undefined') {
|
||||
deleteConfigValue(obj, oldPath);
|
||||
setConfigValue(obj, newPath, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
const UPGRADES = [
|
||||
// v1.2.1 -> v2.0.0
|
||||
upgradeMoveTo('frigate_url', 'frigate.url'),
|
||||
upgradeMoveTo('frigate_client_id', 'frigate.client_id'),
|
||||
upgradeMoveTo('frigate_camera_name', 'frigate.camera_name'),
|
||||
upgradeMoveTo('label', 'frigate.label'),
|
||||
upgradeMoveTo('zone', 'frigate.zone'),
|
||||
upgradeMoveTo('view_default', 'view.default'),
|
||||
upgradeMoveTo('view_timeout', 'view.timeout'),
|
||||
upgradeMoveTo('live_provider', 'live.provider'),
|
||||
upgradeMoveTo('live_preload', 'live.preload'),
|
||||
upgradeMoveTo('webrtc', 'live.webrtc'),
|
||||
upgradeMoveTo('autoplay_clip', 'event_viewer.autoplay_clip'),
|
||||
upgradeMoveTo('controls.nextprev', 'event_viewer.controls.next_previous.style'),
|
||||
upgradeMoveTo('controls.nextprev_size', 'event_viewer.controls.next_previous.size'),
|
||||
upgradeMoveTo('menu_mode', 'menu.mode'),
|
||||
upgradeMoveTo('menu_buttons', 'menu.buttons'),
|
||||
upgradeMoveTo('menu_button_size', 'menu.button_size'),
|
||||
upgradeMoveTo('image', 'image.src', isNotObject),
|
||||
];
|
||||
+269
-135
@@ -4,15 +4,40 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
|
||||
import { localize } from './localize/localize.js';
|
||||
import {
|
||||
FrigateCardConfig,
|
||||
frigateCardConfigDefaults,
|
||||
frigateCardConfigSchema,
|
||||
} from './types.js';
|
||||
import { frigateCardConfigDefaults, RawFrigateCardConfig } from './types.js';
|
||||
|
||||
import frigate_card_editor_style from './scss/editor.scss';
|
||||
import {
|
||||
copyConfig,
|
||||
deleteConfigValue,
|
||||
getConfigValue,
|
||||
isConfigUpgradeable,
|
||||
setConfigValue,
|
||||
trimConfig,
|
||||
upgradeConfig,
|
||||
} from './config-mgmt.js';
|
||||
|
||||
const options = {
|
||||
interface EditorOptionsSet {
|
||||
icon: string;
|
||||
name: string;
|
||||
secondary: string;
|
||||
show: boolean;
|
||||
}
|
||||
interface EditorOptions {
|
||||
[setName: string]: EditorOptionsSet;
|
||||
}
|
||||
|
||||
interface EditorOptionTarget {
|
||||
configValue: string;
|
||||
checked?: boolean;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface EditorOptionSetTarget {
|
||||
optionSetName: string;
|
||||
}
|
||||
|
||||
const options: EditorOptions = {
|
||||
basic: {
|
||||
icon: 'cog',
|
||||
name: localize('editor.basic'),
|
||||
@@ -58,7 +83,7 @@ const options = {
|
||||
dimensions: {
|
||||
icon: 'aspect-ratio',
|
||||
name: localize('editor.dimensions'),
|
||||
secondary: localize('editor.dimensions'),
|
||||
secondary: localize('editor.dimensions_secondary'),
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
@@ -66,16 +91,18 @@ const options = {
|
||||
@customElement('frigate-card-editor')
|
||||
export class FrigateCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state() private _config?: FrigateCardConfig;
|
||||
@state() private _toggle?: boolean;
|
||||
@state() private _helpers?: any;
|
||||
private _initialized = false;
|
||||
@state() protected _config?: RawFrigateCardConfig;
|
||||
@state() protected _helpers?: any;
|
||||
protected _initialized = false;
|
||||
protected _configUpgradeable = false;
|
||||
|
||||
public setConfig(config: FrigateCardConfig): void {
|
||||
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.
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -102,7 +129,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this._helpers) {
|
||||
if (!this.hass || !this._helpers || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -111,11 +138,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
this._helpers.importMoreInfoControl('climate');
|
||||
|
||||
const cameraEntities = this._getEntities('camera');
|
||||
|
||||
const webrtcCameraEntity =
|
||||
this._config?.live?.webrtc && (this._config?.live.webrtc as any).entity
|
||||
? (this._config?.live.webrtc as any).entity
|
||||
: '';
|
||||
const webrtcCameraEntity = String(
|
||||
getConfigValue(this._config, 'live.webrtc.entity', ''),
|
||||
);
|
||||
|
||||
const viewModes = {
|
||||
'': '',
|
||||
@@ -172,8 +197,35 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
const defaults = frigateCardConfigDefaults;
|
||||
|
||||
return html`
|
||||
${this._configUpgradeable
|
||||
? html` <div class="upgrade">
|
||||
<span>${localize('editor.upgrade_available')}</span>
|
||||
<span>
|
||||
<mwc-button
|
||||
raised
|
||||
label="${localize('editor.upgrade')}"
|
||||
@click=${() => {
|
||||
if (this._config) {
|
||||
const upgradedConfig = copyConfig(this._config);
|
||||
upgradeConfig(upgradedConfig);
|
||||
this._config = upgradedConfig;
|
||||
|
||||
fireEvent(this, 'config-changed', { config: this._config });
|
||||
this.requestUpdate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
</mwc-button>
|
||||
</span>
|
||||
</div>
|
||||
<br />`
|
||||
: html``}
|
||||
<div class="card-config">
|
||||
<div class="option" @click=${this._toggleOption} .option=${'basic'}>
|
||||
<div
|
||||
class="option"
|
||||
@click=${this._toggleOptionHandler}
|
||||
.optionSetName=${'basic'}
|
||||
>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.basic.icon}`}></ha-icon>
|
||||
<div class="title">${options.basic.name}</div>
|
||||
@@ -185,13 +237,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<div class="values">
|
||||
<paper-dropdown-menu
|
||||
label=${localize('config.camera_entity')}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
.configValue=${'camera_entity'}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${cameraEntities.indexOf(
|
||||
this._config?.camera_entity || '',
|
||||
String(getConfigValue(this._config, 'camera_entity', '')),
|
||||
)}
|
||||
>
|
||||
${cameraEntities.map((entity) => {
|
||||
@@ -202,7 +254,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="option" @click=${this._toggleOption} .option=${'frigate'}>
|
||||
<div
|
||||
class="option"
|
||||
@click=${this._toggleOptionHandler}
|
||||
.optionSetName=${'frigate'}
|
||||
>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.frigate.icon}`}></ha-icon>
|
||||
<div class="title">${options.frigate.name}</div>
|
||||
@@ -214,38 +270,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<div class="values">
|
||||
<paper-input
|
||||
label=${localize('config.frigate.camera_name')}
|
||||
.value=${this._config?.frigate?.camera_name || ''}
|
||||
.value=${getConfigValue(this._config, 'frigate.camera_name', '')}
|
||||
.configValue=${'frigate.camera_name'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
label=${localize('config.frigate.url')}
|
||||
.value=${this._config?.frigate?.url || ''}
|
||||
.value=${getConfigValue(this._config, 'frigate.url', '')}
|
||||
.configValue=${'frigate.url'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.label=${localize('config.frigate.label')}
|
||||
.value=${this._config?.frigate?.label || ''}
|
||||
.value=${getConfigValue(this._config, 'frigate.label', '')}
|
||||
.configValue=${'frigate.label'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.label=${localize('config.frigate.zone')}
|
||||
.value=${this._config?.frigate?.zone || ''}
|
||||
.value=${getConfigValue(this._config, 'frigate.zone', '')}
|
||||
.configValue=${'frigate.zone'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
label=${localize('config.frigate.client_id')}
|
||||
.value=${this._config?.frigate?.client_id || ''}
|
||||
.value=${getConfigValue(this._config, 'frigate.client_id', '')}
|
||||
.configValue=${'frigate.client_id'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="option" @click=${this._toggleOption} .option=${'view'}>
|
||||
<div class="option" @click=${this._toggleOptionHandler} .optionSetName=${'view'}>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.view.icon}`}></ha-icon>
|
||||
<div class="title">${options.view.name}</div>
|
||||
@@ -257,13 +313,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<div class="values">
|
||||
<paper-dropdown-menu
|
||||
label=${localize('config.view.default')}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
.configValue=${'view.default'}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${Object.keys(viewModes).indexOf(
|
||||
this._config?.view?.default || '',
|
||||
String(getConfigValue(this._config, 'view.default', '')),
|
||||
)}
|
||||
>
|
||||
${Object.keys(viewModes).map((key) => {
|
||||
@@ -277,16 +333,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
label=${localize('config.view.timeout')}
|
||||
prevent-invalid-input
|
||||
allowed-pattern="[0-9]"
|
||||
.value=${this._config?.view?.timeout
|
||||
? String(this._config?.view?.timeout)
|
||||
: ''}
|
||||
.value=${getConfigValue(this._config, 'view.timeout', '')}
|
||||
.configValue=${'view.timeout'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="option" @click=${this._toggleOption} .option=${'menu'}>
|
||||
<div class="option" @click=${this._toggleOptionHandler} .optionSetName=${'menu'}>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.menu.icon}`}></ha-icon>
|
||||
<div class="title">${options.menu.name}</div>
|
||||
@@ -298,13 +352,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<div class="values">
|
||||
<paper-dropdown-menu
|
||||
.label=${localize('config.menu.mode')}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
.configValue=${'menu.mode'}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${Object.keys(menuModes).indexOf(
|
||||
this._config?.menu?.mode || '',
|
||||
String(getConfigValue(this._config, 'menu.mode', '')),
|
||||
)}
|
||||
>
|
||||
${Object.keys(menuModes).map((key) => {
|
||||
@@ -316,9 +370,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
</paper-dropdown-menu>
|
||||
<paper-input
|
||||
label=${localize('config.menu.button_size')}
|
||||
.value=${this._config?.menu?.button_size || ''}
|
||||
.value=${getConfigValue(this._config, 'menu.button_size', '')}
|
||||
.configValue=${'menu.button_size'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
<ha-formfield
|
||||
.label=${localize('editor.show_button') +
|
||||
@@ -326,10 +380,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.menu.buttons.frigate')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.frigate ??
|
||||
defaults.menu.buttons.frigate}
|
||||
.checked="${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.frigate',
|
||||
defaults.menu.buttons.frigate,
|
||||
)},"
|
||||
.configValue=${'menu.buttons.frigate'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -338,10 +395,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.view.views.live')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.live ??
|
||||
defaults.menu.buttons.live}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.live',
|
||||
defaults.menu.buttons.live,
|
||||
)}
|
||||
.configValue=${'menu.buttons.live'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -350,10 +410,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.view.views.clips')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.clips ??
|
||||
defaults.menu.buttons.clips}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.clips',
|
||||
defaults.menu.buttons.clips,
|
||||
)}
|
||||
.configValue=${'menu.buttons.clips'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -362,10 +425,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.view.views.snapshots')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.snapshots ??
|
||||
defaults.menu.buttons.snapshots}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.snapshots',
|
||||
defaults.menu.buttons.snapshots,
|
||||
)}
|
||||
.configValue=${'menu.buttons.snapshots'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -374,10 +440,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.view.views.image')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.image ??
|
||||
defaults.menu.buttons.image}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.image',
|
||||
defaults.menu.buttons.image,
|
||||
)}
|
||||
.configValue=${'menu.buttons.image'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -386,10 +455,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.menu.buttons.download')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.download ??
|
||||
defaults.menu.buttons.download}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.download',
|
||||
defaults.menu.buttons.download,
|
||||
)}
|
||||
.configValue=${'menu.buttons.download'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -398,10 +470,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.menu.buttons.frigate_ui')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.frigate_ui ??
|
||||
defaults.menu.buttons.frigate_ui}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.frigate_ui',
|
||||
defaults.menu.buttons.frigate_ui,
|
||||
)}
|
||||
.configValue=${'menu.buttons.frigate_ui'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -410,16 +485,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
localize('config.menu.buttons.fullscreen')}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config?.menu?.buttons?.fullscreen ??
|
||||
defaults.menu.buttons.fullscreen}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'menu.buttons.fullscreen',
|
||||
defaults.menu.buttons.fullscreen,
|
||||
)}
|
||||
.configValue=${'menu.buttons.fullscreen'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="option" @click=${this._toggleOption} .option=${'live'}>
|
||||
<div class="option" @click=${this._toggleOptionHandler} .optionSetName=${'live'}>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.live.icon}`}></ha-icon>
|
||||
<div class="title">${options.live.name}</div>
|
||||
@@ -432,20 +510,24 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<br />
|
||||
<ha-formfield .label=${localize('config.live.preload')}>
|
||||
<ha-switch
|
||||
.checked=${this._config?.live?.preload ?? defaults.live.preload}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'live.preload',
|
||||
defaults.live.preload,
|
||||
)}
|
||||
.configValue=${'live.preload'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<paper-dropdown-menu
|
||||
.label=${localize('config.live.provider')}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
.configValue=${'live.provider'}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${Object.keys(liveProviders).indexOf(
|
||||
this._config?.live?.provider || '',
|
||||
String(getConfigValue(this._config, 'live.provider', '')),
|
||||
)}
|
||||
>
|
||||
${Object.keys(liveProviders).map((key) => {
|
||||
@@ -457,7 +539,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
</paper-dropdown-menu>
|
||||
<paper-dropdown-menu
|
||||
.label=${localize('config.live.webrtc.entity')}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
.configValue=${'live.webrtc.entity'}
|
||||
>
|
||||
<paper-listbox
|
||||
@@ -471,14 +553,18 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
</paper-dropdown-menu>
|
||||
<paper-input
|
||||
label=${localize('config.live.webrtc.url')}
|
||||
.value=${this._config?.live?.webrtc?.url || ''}
|
||||
.value=${getConfigValue(this._config, 'live.webrtc.url', '')}
|
||||
.configValue=${'live.webrtc.url'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="option" @click=${this._toggleOption} .option=${'event_viewer'}>
|
||||
<div
|
||||
class="option"
|
||||
@click=${this._toggleOptionHandler}
|
||||
.optionSetName=${'event_viewer'}
|
||||
>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.event_viewer.icon}`}></ha-icon>
|
||||
<div class="title">${options.event_viewer.name}</div>
|
||||
@@ -490,29 +576,41 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<br />
|
||||
<ha-formfield .label=${localize('config.event_viewer.autoplay_clip')}>
|
||||
<ha-switch
|
||||
.checked=${this._config?.event_viewer?.autoplay_clip ??
|
||||
defaults.event_viewer.autoplay_clip}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'event_viewer.autoplay_clip',
|
||||
defaults.event_viewer.autoplay_clip,
|
||||
)}
|
||||
.configValue=${'event_viewer.autoplay_clip'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield .label=${localize('config.event_viewer.lazy_load')}>
|
||||
<ha-switch
|
||||
.checked=${this._config?.event_viewer?.lazy_load ??
|
||||
defaults.event_viewer.lazy_load}
|
||||
.checked=${getConfigValue(
|
||||
this._config,
|
||||
'event_viewer.lazy_load',
|
||||
defaults.event_viewer.lazy_load,
|
||||
)}
|
||||
.configValue=${'event_viewer.lazy_load'}
|
||||
@change=${this._valueChanged}
|
||||
@change=${this._valueChangedHandler}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<paper-dropdown-menu
|
||||
.label=${localize('config.event_viewer.controls.next_previous.style')}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
.configValue=${'event_viewer.controls.next_previous.style'}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${Object.keys(eventViewerNextPreviousControlStyles).indexOf(
|
||||
this._config?.event_viewer?.controls?.next_previous?.style || '',
|
||||
String(
|
||||
getConfigValue(
|
||||
this._config,
|
||||
'event_viewer.controls.next_previous.style',
|
||||
'',
|
||||
),
|
||||
),
|
||||
)}
|
||||
>
|
||||
${Object.keys(eventViewerNextPreviousControlStyles).map((key) => {
|
||||
@@ -526,13 +624,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
</paper-dropdown-menu>
|
||||
<paper-input
|
||||
label=${localize('config.event_viewer.controls.next_previous.size')}
|
||||
.value=${this._config?.event_viewer?.controls?.next_previous?.size || ''}
|
||||
.value=${getConfigValue(
|
||||
this._config,
|
||||
'event_viewer.controls.next_previous.size',
|
||||
'',
|
||||
)}
|
||||
.configValue=${'event_viewer.controls.next_previous.size'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
</div>`
|
||||
: ''}
|
||||
<div class="option" @click=${this._toggleOption} .option=${'image'}>
|
||||
<div
|
||||
class="option"
|
||||
@click=${this._toggleOptionHandler}
|
||||
.optionSetName=${'image'}
|
||||
>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.image.icon}`}></ha-icon>
|
||||
<div class="title">${options.image.name}</div>
|
||||
@@ -544,15 +650,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<paper-input
|
||||
label=${localize('config.image.src')}
|
||||
prevent-invalid-input
|
||||
.value=${this._config?.image?.src
|
||||
? String(this._config?.image?.src)
|
||||
: ''}
|
||||
.value=${getConfigValue(this._config, 'image.src', '')}
|
||||
.configValue=${'image.src'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
</div>`
|
||||
: ''}
|
||||
<div class="option" @click=${this._toggleOption} .option=${'dimensions'}>
|
||||
<div
|
||||
class="option"
|
||||
@click=${this._toggleOptionHandler}
|
||||
.optionSetName=${'dimensions'}
|
||||
>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${options.dimensions.icon}`}></ha-icon>
|
||||
<div class="title">${options.dimensions.name}</div>
|
||||
@@ -563,13 +671,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
? html` <div class="values">
|
||||
<paper-dropdown-menu
|
||||
.label=${localize('config.dimensions.aspect_ratio_mode')}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
.configValue=${'dimensions.aspect_ratio_mode'}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${Object.keys(aspectRatioModes).indexOf(
|
||||
this._config?.dimensions?.aspect_ratio_mode || '',
|
||||
String(
|
||||
getConfigValue(this._config, 'dimensions.aspect_ratio_mode', ''),
|
||||
),
|
||||
)}
|
||||
>
|
||||
${Object.keys(aspectRatioModes).map((key) => {
|
||||
@@ -582,11 +692,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
<paper-input
|
||||
label=${localize('config.dimensions.aspect_ratio')}
|
||||
prevent-invalid-input
|
||||
.value=${this._config?.dimensions?.aspect_ratio
|
||||
? String(this._config?.dimensions?.aspect_ratio)
|
||||
: ''}
|
||||
.value=${getConfigValue(this._config, 'dimensions.aspect_ratio', '')}
|
||||
.configValue=${'dimensions.aspect_ratio'}
|
||||
@value-changed=${this._valueChanged}
|
||||
@value-changed=${this._valueChangedHandler}
|
||||
></paper-input>
|
||||
</div>`
|
||||
: ''}
|
||||
@@ -594,69 +702,95 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
`;
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
private async loadCardHelpers(): Promise<void> {
|
||||
/**
|
||||
* Load card helpers.
|
||||
*/
|
||||
protected async loadCardHelpers(): Promise<void> {
|
||||
this._helpers = await (window as any).loadCardHelpers();
|
||||
}
|
||||
|
||||
private _toggleOption(ev): void {
|
||||
this._toggleThing(ev, options);
|
||||
/**
|
||||
* Handle a toggled set of options.
|
||||
* @param ev The event triggering the change.
|
||||
*/
|
||||
protected _toggleOptionHandler(ev: { target: EditorOptionSetTarget | null }): void {
|
||||
this._toggleOptionSet(ev, options);
|
||||
}
|
||||
|
||||
private _toggleThing(ev, optionList): void {
|
||||
const show = !optionList[ev.target.option].show;
|
||||
for (const [key] of Object.entries(optionList)) {
|
||||
optionList[key].show = false;
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
optionList[ev.target.option].show = show;
|
||||
this._toggle = !this._toggle;
|
||||
}
|
||||
|
||||
private _valueChanged(ev): void {
|
||||
if (!this._config || !this.hass) {
|
||||
/**
|
||||
* Handle a changed option value.
|
||||
* @param ev Event triggering the change.
|
||||
* @returns
|
||||
*/
|
||||
protected _valueChangedHandler(ev: {
|
||||
target: (EditorOptionTarget & HTMLElement) | null;
|
||||
}): void {
|
||||
const target = ev.target;
|
||||
if (!this._config || !this.hass || !target) {
|
||||
return;
|
||||
}
|
||||
const target = ev.target;
|
||||
const value = target.value?.trim();
|
||||
let key: string = target.configValue;
|
||||
|
||||
let value;
|
||||
if ('checked' in target) {
|
||||
value = target.checked;
|
||||
} else {
|
||||
value = target.value?.trim();
|
||||
}
|
||||
const key: string = target.configValue;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to deep copy the config so cannot use Object.assign.
|
||||
const newConfig = JSON.parse(JSON.stringify(this._config));
|
||||
let objectTarget = newConfig;
|
||||
|
||||
if (key.includes('.')) {
|
||||
const parts = key.split('.');
|
||||
objectTarget = parts.slice(0, -1).reduce((obj, key) => {
|
||||
if (!(key in obj)) {
|
||||
obj[key] = {};
|
||||
const newConfig = copyConfig(this._config);
|
||||
if (value === '' || typeof value === 'undefined') {
|
||||
// Don't delete empty properties that are from a dropdown menu. An empty
|
||||
// property in that context may just be a user-entered value that is not
|
||||
// in the valid choices in the dropdown. This probably won't end well for
|
||||
// the user anyway, but having the whole property deleted the moment they
|
||||
// press a key is very jarring.
|
||||
if (target.tagName != 'PAPER-DROPDOWN-MENU') {
|
||||
deleteConfigValue(newConfig, key);
|
||||
}
|
||||
return obj[key];
|
||||
}, newConfig);
|
||||
key = parts[parts.length - 1];
|
||||
}
|
||||
|
||||
if (value !== undefined && objectTarget[key] === value) {
|
||||
return;
|
||||
} else if (value === '') {
|
||||
delete objectTarget[key];
|
||||
} else {
|
||||
objectTarget[key] = target.checked !== undefined ? target.checked : value;
|
||||
setConfigValue(newConfig, key, value);
|
||||
}
|
||||
trimConfig(newConfig);
|
||||
this._config = newConfig;
|
||||
fireEvent(this, 'config-changed', { config: this._config });
|
||||
}
|
||||
|
||||
// Return compiled CSS styles (thus safe to use with unsafeCSS).
|
||||
/**
|
||||
* Return compiled CSS styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(frigate_card_editor_style);
|
||||
}
|
||||
|
||||
@@ -101,22 +101,25 @@
|
||||
},
|
||||
"editor": {
|
||||
"basic": "Basic",
|
||||
"basic_secondary": "Basic options for most users",
|
||||
"basic_secondary": "Options for most users",
|
||||
"frigate": "Frigate",
|
||||
"frigate_secondary": "Frigate server options",
|
||||
"view": "View",
|
||||
"view_secondary": "Card view options",
|
||||
"view_secondary": "What the card should show and how to show it",
|
||||
"menu": "Menu",
|
||||
"menu_secondary": "Menu options",
|
||||
"menu_secondary": "Menu look & feel options",
|
||||
"live": "Live",
|
||||
"live_secondary": "Live view options",
|
||||
"live_secondary": "Live camera view options",
|
||||
"event_viewer": "Event viewer",
|
||||
"event_viewer_secondary": "Event viewer options",
|
||||
"event_viewer_secondary": "Snapshots & clips gallery options",
|
||||
"image": "Image",
|
||||
"image_secondary": "Static image view options",
|
||||
"dimensions": "Dimensions",
|
||||
"dimensions_secondary": "Card dimensions options",
|
||||
"show_button": "Show button"
|
||||
"dimensions_secondary": "Dimensions & shape options",
|
||||
"show_button": "Show button",
|
||||
"upgrade": "Upgrade",
|
||||
"upgrade_available": "An automatic card configuration upgrade is available",
|
||||
"upgrade_available_in_editor": "An automatic card configuration upgrade is available in the editor"
|
||||
},
|
||||
"error": {
|
||||
"empty_response": "Received empty response from Home Assistant for request",
|
||||
|
||||
@@ -25,3 +25,15 @@
|
||||
ha-formfield {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
div.upgrade {
|
||||
width: auto;
|
||||
border: 1px dotted var(--primary-color);
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
div.upgrade span {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
:host {
|
||||
--video-max-height: none;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
@@ -466,6 +466,7 @@ export const frigateCardConfigSchema = z.object({
|
||||
test_gui: z.boolean().optional(),
|
||||
});
|
||||
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
|
||||
export type RawFrigateCardConfig = Record<string, unknown>;
|
||||
|
||||
export const frigateCardConfigDefaults = {
|
||||
frigate: frigateConfigDefault,
|
||||
|
||||
Reference in New Issue
Block a user