diff --git a/rollup.config.js b/rollup.config.js index f75ade85..58bfaa3c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -41,7 +41,7 @@ const plugins = [ exclude: 'node_modules/**', }), dev && serve(serveopts), - !dev && terser(), + //!dev && terser(), ]; export default [ diff --git a/src/card.ts b/src/card.ts index 4b1efcd3..323f0409 100644 --- a/src/card.ts +++ b/src/card.ts @@ -429,8 +429,8 @@ export class FrigateCard extends LitElement { } }; - if (this.config.camera && Array.isArray(this.config.camera)) { - await Promise.all(this.config.camera.map(addCameraConfig.bind(this))); + if (this.config.cameras && Array.isArray(this.config.cameras)) { + await Promise.all(this.config.cameras.map(addCameraConfig.bind(this))); } if (!cameras.size) { diff --git a/src/common.ts b/src/common.ts index 81127e73..2cb7a907 100644 --- a/src/common.ts +++ b/src/common.ts @@ -287,7 +287,8 @@ export function convertActionToFrigateCardCustomAction( */ export function createFrigateCardCustomAction( action: FrigateCardAction, - camera?: string): FrigateCardCustomAction | undefined { + camera?: string, +): FrigateCardCustomAction | undefined { if (action == 'camera_select') { if (!camera) { return undefined; @@ -296,12 +297,12 @@ export function createFrigateCardCustomAction( action: 'fire-dom-event', frigate_card_action: action, camera: camera, - } + }; } return { action: 'fire-dom-event', frigate_card_action: action, - } + }; } /** @@ -418,3 +419,15 @@ export function refreshCameraConfigDynamicParameters( config.icon = config.icon ?? (state ? stateIcon(state) : 'mdi:video'); return config; } + +/** + * Move an element within an array. + * @param target Target array. + * @param from From index. + * @param to To index. + */ +export function arrayMove(target: unknown[], from: number, to: number): void { + const element = target[from]; + target.splice(from, 1); + target.splice(to, 0, element); +} diff --git a/src/components/live.ts b/src/components/live.ts index 31f50a88..afe6a53b 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -1,7 +1,9 @@ // TODO editor +// TODO editor: fill in a new camera and keep focus +// TODO editor: fill in a new camera then backspace it away // TODO webrtc entities in camera section? // TODO conditional elements based on camera name (requires event changed to propagate upwards) -// TODO change url to frigate_url? +// TODO change url to frigate_url? Would need to also fix upgrade logic to refer to new name. // TODO Remove media load event warning // TODO readme // TODO search for TODOs diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts index d9691677..adbbb9f1 100644 --- a/src/config-mgmt.ts +++ b/src/config-mgmt.ts @@ -1,14 +1,16 @@ import delve from 'dlv'; import { dset } from 'dset'; import { + CONF_CAMERAS, + CONF_CAMERAS_ARRAY_CAMERA_ENTITY, + CONF_CAMERAS_ARRAY_CAMERA_NAME, + CONF_CAMERAS_ARRAY_CLIENT_ID, + CONF_CAMERAS_ARRAY_LABEL, + CONF_CAMERAS_ARRAY_URL, + CONF_CAMERAS_ARRAY_ZONE, CONF_EVENT_VIEWER_AUTOPLAY_CLIP, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, - CONF_FRIGATE_CAMERA_NAME, - CONF_FRIGATE_CLIENT_ID, - CONF_FRIGATE_LABEL, - CONF_FRIGATE_URL, - CONF_FRIGATE_ZONE, CONF_IMAGE_SRC, CONF_LIVE_PRELOAD, CONF_LIVE_PROVIDER, @@ -18,7 +20,7 @@ import { CONF_VIEW_TIMEOUT, CONF_VIEW_UPDATE_ENTITIES, } from './const'; -import { RawFrigateCardConfig } from './types'; +import { RawFrigateCardConfig, RawFrigateCardConfigArray } from './types'; /** * Set a configuration value. @@ -26,12 +28,13 @@ import { RawFrigateCardConfig } from './types'; * @param key The key to the property to set. * @param value The value to set. */ + export const setConfigValue = ( obj: RawFrigateCardConfig, - key: string, + keys: string | (string|number)[], value: unknown, ): void => { - dset(obj, key, value); + dset(obj, keys, value); }; /** @@ -42,10 +45,14 @@ export const setConfigValue = ( */ export const getConfigValue = ( obj: RawFrigateCardConfig, - key: string, + keys: string | (string|number)[], def?: unknown, ): unknown => { - return delve(obj, key, def); + // Need to manually split the key apart to use delve array accesses by number. + if (typeof(keys) === 'string') { + keys = keys.split('.'); + } + return delve(obj, keys, def); }; /** @@ -93,19 +100,23 @@ export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean /** * Remove empty sections from a configuration. * @param obj Configuration object. + * @returns `true` if the configuration was modified. */ -export const trimConfig = function (obj: RawFrigateCardConfig): void { +export const trimConfig = function (obj: RawFrigateCardConfig): boolean { const keys = Object.keys(obj); + let modified = false; 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); + modified ||= trimConfig(obj[key] as RawFrigateCardConfig); if (!Object.keys(obj[key] as RawFrigateCardConfig).length) { delete obj[key]; + modified = true; } } } + return modified; }; /** @@ -128,6 +139,32 @@ const isNotObject = function (value: unknown) { /** * Move a property from one location to another. + * @param obj The configuration object in which the property resides. + * @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. + */ +export const moveConfigValue = ( + obj: RawFrigateCardConfig, + oldPath: string, + newPath: string, + transform?: (valueIn: unknown) => (unknown), +): 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; +}; + +/** + * Upgrade by moving 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. @@ -136,29 +173,66 @@ const isNotObject = function (value: unknown) { const upgradeMoveTo = function ( oldPath: string, newPath: string, - transform?: (valueIn: unknown) => unknown, + 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; + return moveConfigValue(obj, oldPath, newPath, transform); }; }; +/** + * Sanitize a potentially-unsafe key segment. + * @param key A string key. + * @returns A safe key. + */ +// const sanitizeKeySegment = (key: string): string => { +// return key.replace(/\.+/g, '_'); +// } + +const upgradeToMultipleCameras = (): ((obj: RawFrigateCardConfig) => boolean) => { + return function (obj: RawFrigateCardConfig): boolean { + let modified = false; + + const camera = {} + const imports = { + 'camera_entity': CONF_CAMERAS_ARRAY_CAMERA_ENTITY, + 'frigate.camera_name': CONF_CAMERAS_ARRAY_CAMERA_NAME, + 'frigate.client_id': CONF_CAMERAS_ARRAY_CLIENT_ID, + 'frigate.label': CONF_CAMERAS_ARRAY_LABEL, + 'frigate.url': CONF_CAMERAS_ARRAY_URL, + 'frigate.zone': CONF_CAMERAS_ARRAY_ZONE, + } + Object.keys(imports).forEach((key) => { + const oldValue = getConfigValue(obj, key); + if (oldValue !== undefined) { + camera[imports[key]] = oldValue; + deleteConfigValue(obj, key) + modified = true; + } + }) + + if (modified) { + let cameras = getConfigValue(obj, CONF_CAMERAS) as RawFrigateCardConfigArray; + if (!Array.isArray(cameras)) { + // Note: This will replace `cameras` if it already exists and isn't an + // array. + cameras = [] + } + cameras.push(camera); + setConfigValue(obj, CONF_CAMERAS, cameras) + trimConfig(obj); + } + return modified; + } +} + const UPGRADES = [ // v1.2.1 -> v2.0.0 - upgradeMoveTo('frigate_url', CONF_FRIGATE_URL), - upgradeMoveTo('frigate_client_id', CONF_FRIGATE_CLIENT_ID), - upgradeMoveTo('frigate_camera_name', CONF_FRIGATE_CAMERA_NAME), - upgradeMoveTo('label', CONF_FRIGATE_LABEL), - upgradeMoveTo('zone', CONF_FRIGATE_ZONE), + 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', CONF_VIEW_DEFAULT), upgradeMoveTo('view_timeout', CONF_VIEW_TIMEOUT), upgradeMoveTo('live_provider', CONF_LIVE_PROVIDER), @@ -174,4 +248,7 @@ const UPGRADES = [ // v2.0.0 -> v2.1.0 upgradeMoveTo('update_entities', CONF_VIEW_UPDATE_ENTITIES), + + // v2.1.0 -> v3.0.0 + upgradeToMultipleCameras(), ]; diff --git a/src/const.ts b/src/const.ts index 9a3cea39..342ace13 100644 --- a/src/const.ts +++ b/src/const.ts @@ -2,12 +2,13 @@ export const CARD_VERSION = '2.1.0'; export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card'; export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting`; -export const CONF_CAMERA_ENTITY = 'camera_entity'; -export const CONF_FRIGATE_CAMERA_NAME = 'frigate.camera_name'; -export const CONF_FRIGATE_CLIENT_ID = 'frigate.client_id'; -export const CONF_FRIGATE_LABEL = 'frigate.label'; -export const CONF_FRIGATE_URL = 'frigate.url'; -export const CONF_FRIGATE_ZONE = 'frigate.zone'; +export const CONF_CAMERAS = 'cameras'; +export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY = 'cameras.#.camera_entity'; +export const CONF_CAMERAS_ARRAY_CAMERA_NAME = 'cameras.#.camera_name'; +export const CONF_CAMERAS_ARRAY_CLIENT_ID = 'cameras.#.client_id'; +export const CONF_CAMERAS_ARRAY_LABEL = 'cameras.#.label'; +export const CONF_CAMERAS_ARRAY_URL = 'cameras.#.url'; +export const CONF_CAMERAS_ARRAY_ZONE = 'cameras.#.zone'; export const CONF_VIEW_DEFAULT = 'view.default'; export const CONF_VIEW_TIMEOUT = 'view.timeout'; diff --git a/src/editor.ts b/src/editor.ts index 7d3d7ba5..078d92b7 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -5,20 +5,20 @@ 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 } from './types.js'; +import { + frigateCardConfigDefaults, + RawFrigateCardConfig, + RawFrigateCardConfigArray, +} from './types.js'; -import frigate_card_editor_style from './scss/editor.scss'; import { - copyConfig, - deleteConfigValue, - getConfigValue, - isConfigUpgradeable, - setConfigValue, - trimConfig, - upgradeConfig, -} from './config-mgmt.js'; -import { - CONF_CAMERA_ENTITY, + CONF_CAMERAS, + CONF_CAMERAS_ARRAY_CAMERA_ENTITY, + CONF_CAMERAS_ARRAY_CAMERA_NAME, + CONF_CAMERAS_ARRAY_CLIENT_ID, + CONF_CAMERAS_ARRAY_LABEL, + CONF_CAMERAS_ARRAY_URL, + CONF_CAMERAS_ARRAY_ZONE, CONF_DIMENSIONS_ASPECT_RATIO, CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_EVENT_VIEWER_AUTOPLAY_CLIP, @@ -28,11 +28,6 @@ import { CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_EVENT_VIEWER_DRAGGABLE, CONF_EVENT_VIEWER_LAZY_LOAD, - CONF_FRIGATE_CAMERA_NAME, - CONF_FRIGATE_CLIENT_ID, - CONF_FRIGATE_LABEL, - CONF_FRIGATE_URL, - CONF_FRIGATE_ZONE, CONF_IMAGE_SRC, CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, CONF_LIVE_CONTROLS_THUMBNAILS_MODE, @@ -55,6 +50,17 @@ import { CONF_VIEW_TIMEOUT, CONF_VIEW_UPDATE_FORCE, } from './const.js'; +import { arrayMove } from './common.js'; +import { + copyConfig, + deleteConfigValue, + getConfigValue, + isConfigUpgradeable, + setConfigValue, + upgradeConfig, +} from './config-mgmt.js'; + +import frigate_card_editor_style from './scss/editor.scss'; interface EditorOptionsSet { icon: string; @@ -66,29 +72,27 @@ interface EditorOptions { [setName: string]: EditorOptionsSet; } -interface EditorOptionTarget { +interface ConfigValueTarget { configValue: string; checked?: boolean; value?: string; } +interface EditorCameraTarget { + cameraIndex: number; +} + interface EditorOptionSetTarget { optionSetName: string; } const options: EditorOptions = { - basic: { - icon: 'cog', - name: localize('editor.basic'), - secondary: localize('editor.basic_secondary'), + cameras: { + icon: 'video', + name: localize('editor.cameras'), + secondary: localize('editor.cameras_secondary'), show: true, }, - frigate: { - icon: 'alpha-f-box', - name: localize('editor.frigate'), - secondary: localize('editor.frigate_secondary'), - show: false, - }, view: { icon: 'eye', name: localize('editor.view'), @@ -135,6 +139,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor protected _initialized = false; protected _configUpgradeable = false; + @property({ attribute: false }) + protected _expandedCameraIndex: number | null = null; + 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 @@ -190,6 +197,20 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor `; } + /** + * 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 single number path components as they are array indicies. + const path = configPath + .split('.') + .filter((e) => isNaN(Number(e))) + .join('.'); + return localize(`config.${path}`); + } + /** * Render a dropdown menu. * @param configPath The configuration path to set/read. @@ -207,7 +228,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor return html` @@ -225,6 +246,160 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor `; } + /** + * 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 | undefined, + 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` + Camera: + ${cameraConfig?.title || + cameraConfig?.camera_entity || + cameraConfig?.card_id || + [ + cameraConfig?.client_id, + cameraConfig?.camera_name, + cameraConfig?.label, + cameraConfig?.zone, + ] + .filter(Boolean) + .join(' / ') || + 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 { + // Get the config path for this camera (taking into account its camera index). + const getArrayPath = (path: string): string => { + return path.replace('#', cameraIndex.toString()); + }; + + // 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`
+
+ + 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=${() => + 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( + getArrayPath(CONF_CAMERAS_ARRAY_CAMERA_ENTITY), + cameraEntities, + )} + ${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_CAMERA_NAME))} + ${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_URL))} + ${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_LABEL))} + ${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_ZONE))} + ${this._renderStringInput(getArrayPath(CONF_CAMERAS_ARRAY_CLIENT_ID))} +
` + : ``} + `; + } + /** * Render a string input field. * @param configPath The configuration path to set/read. @@ -239,7 +414,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor return; } return html` + 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``; @@ -353,6 +533,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor 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`
@@ -365,10 +548,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor if (this._config) { const upgradedConfig = copyConfig(this._config); upgradeConfig(upgradedConfig); - this._config = upgradedConfig; - - fireEvent(this, 'config-changed', { config: this._config }); - this.requestUpdate(); + this._updateConfig(upgradedConfig); } }} > @@ -378,25 +558,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
` : html``}
- ${this._renderOptionSetHeader('basic')} - ${options.basic.show - ? html` -
- ${this._renderDropdown(CONF_CAMERA_ENTITY, cameraEntities)} -
- ` - : ''} - ${this._renderOptionSetHeader('frigate')} - ${options.frigate.show - ? html` -
- ${this._renderStringInput(CONF_FRIGATE_CAMERA_NAME)} - ${this._renderStringInput(CONF_FRIGATE_URL)} - ${this._renderStringInput(CONF_FRIGATE_LABEL)} - ${this._renderStringInput(CONF_FRIGATE_ZONE)} - ${this._renderStringInput(CONF_FRIGATE_CLIENT_ID)} -
- ` + ${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 @@ -404,9 +573,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderDropdown(CONF_VIEW_DEFAULT, viewModes)} ${this._renderStringInput(CONF_VIEW_TIMEOUT, '[0-9]')} - ${this._renderSwitch( - CONF_VIEW_UPDATE_FORCE, - defaults.view.update_force)} + ${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
` : ''} @@ -541,6 +708,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor 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. @@ -573,7 +753,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor * @param ev Event triggering the change. */ protected _valueChangedHandler(ev: { - target: (EditorOptionTarget & HTMLElement) | null; + target: (ConfigValueTarget & HTMLElement) | null; }): void { const target = ev.target; if (!this._config || !this.hass || !target) { @@ -593,19 +773,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor 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); - } + deleteConfigValue(newConfig, key); } else { setConfigValue(newConfig, key, value); } - this._config = newConfig; - fireEvent(this, 'config-changed', { config: this._config }); + this._updateConfig(newConfig); } /** diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index e598d2e7..c81700ed 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -9,9 +9,9 @@ "no_clip": "No recent clip" }, "config": { - "camera_entity": "Camera Entity", - "frigate": { - "camera_name": "Frigate camera name (Optional, autodetected from entity)", + "cameras": { + "camera_entity": "Camera Entity", + "camera_name": "Frigate camera name (Autodetected from entity)", "client_id": "Frigate client id (For >1 Frigate server)", "label": "Frigate label/object filter", "url": "Frigate server URL", @@ -121,10 +121,8 @@ } }, "editor": { - "basic": "Basic", - "basic_secondary": "Options for most users", - "frigate": "Frigate", - "frigate_secondary": "Frigate server options", + "cameras": "Cameras", + "cameras_secondary": "What cameras to render on this card", "view": "View", "view_secondary": "What the card should show and how to show it", "menu": "Menu", @@ -139,7 +137,11 @@ "dimensions_secondary": "Dimensions & shape options", "show_button": "Show button", "upgrade": "Upgrade", - "upgrade_available": "An automatic card configuration upgrade is available" + "upgrade_available": "An automatic card configuration upgrade is available", + "delete": "Delete", + "move_up": "Move up", + "move_down": "Move down", + "add_new_camera": "Add new camera" }, "error": { "empty_response": "Received empty response from Home Assistant for request", diff --git a/src/scss/editor.scss b/src/scss/editor.scss index 92e45bc4..a2813fa0 100644 --- a/src/scss/editor.scss +++ b/src/scss/editor.scss @@ -1,3 +1,5 @@ +@use './button.scss'; + .option { padding: 4px 0px; cursor: pointer; @@ -18,8 +20,8 @@ pointer-events: none; } .values { - padding-left: 32px; - padding-top: 10px; + margin-left: 30px; + padding: 10px; background: var(--secondary-background-color); display: grid; } @@ -37,4 +39,31 @@ div.upgrade { } div.upgrade span { padding: 10px; +} + +.camera-header { + margin-top: 4px; + cursor: pointer; +} +.camera-header * { + // Only allow clicks on the outermost header. + pointer-events: none; +} +.camera-header ha-icon { + padding-right: 10px; +} +.camera-header .new-camera { + font-style: italic; +} +.cameras { + margin: 5px 5px 10px 20px; +} +.cameras .controls { + display: inline-block; + margin-left: auto; + margin-right: 0px; +} +.cameras .controls ha-icon-button.button { + --mdc-icon-button-size: 32px; + --mdc-icon-size: calc(var(--mdc-icon-button-size) / 2); } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 98501ec0..68bc2b20 100644 --- a/src/types.ts +++ b/src/types.ts @@ -650,7 +650,7 @@ const dimensionsConfigSchema = z */ export const frigateCardConfigSchema = z.object({ // Main configuration sections. - camera: cameraConfigDefaultSchema.array().nonempty(), + cameras: cameraConfigDefaultSchema.array().nonempty(), view: viewConfigSchema, menu: menuConfigSchema, live: liveConfigSchema, @@ -666,6 +666,7 @@ export const frigateCardConfigSchema = z.object({ }); export type FrigateCardConfig = z.infer; export type RawFrigateCardConfig = Record; +export type RawFrigateCardConfigArray = Record[]; export const frigateCardConfigDefaults = { cameras: cameraConfigDefault,