diff --git a/README.md b/README.md index f5954a61..adf07ca8 100644 --- a/README.md +++ b/README.md @@ -874,7 +874,7 @@ All variables listed are under a `conditions:` section. | `camera` | A list of camera ids in which this condition is satisfied. See [camera IDs](#camera-ids).| | `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode.| | `state` | A list of state conditions to compare with Home Assistant state. See below. | -| `mediaLoaded` | If `true` the condition is satisfied if there is media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. Note that if `true` this condition will never be satisfied for views that do not themselves load media directly (e.g. gallery).| +| `media_loaded` | If `true` the condition is satisfied if there is media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. Note that if `true` this condition will never be satisfied for views that do not themselves load media directly (e.g. gallery).| | `media_query` | Any valid [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) string. Media queries must start and end with parentheses. This may be used to alter card configuration based on device/media properties (e.g. viewport width, orientation). Please note that `width` and `height` refer to the entire viewport not just the card. See the [media query example](#media-query-example).| See the [example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used. @@ -1842,7 +1842,7 @@ elements: - entity: light.office_main_lights state: on state_not: off - mediaLoaded: true + media_loaded: true # Full form PTZ actions (only left button shown). - type: custom:frigate-card-ptz orientation: vertical @@ -2733,7 +2733,7 @@ This example shows the native PTZ element when the `live` or `image` view is dis elements: - type: custom:frigate-card-conditional conditions: - mediaLoaded: true + media_loaded: true view: - live - image diff --git a/src/card-condition.ts b/src/card-condition.ts index b5af413c..36ecf49a 100644 --- a/src/card-condition.ts +++ b/src/card-condition.ts @@ -14,7 +14,7 @@ export interface ConditionState { fullscreen?: boolean; camera?: string; state?: HassEntities; - mediaLoaded?: boolean; + media_loaded?: boolean; } class ConditionStateRequestEvent extends Event { @@ -52,9 +52,9 @@ export function evaluateCondition( state.state[stateTest.entity].state !== stateTest.state_not))); } } - if (condition?.mediaLoaded !== undefined) { + if (condition?.media_loaded !== undefined) { result &&= - state.mediaLoaded !== undefined && condition.mediaLoaded == state.mediaLoaded; + state.media_loaded !== undefined && condition.media_loaded == state.media_loaded; } if (condition?.media_query) { result &&= window.matchMedia(condition.media_query).matches; diff --git a/src/card.ts b/src/card.ts index 48b86397..d7ee8069 100644 --- a/src/card.ts +++ b/src/card.ts @@ -285,7 +285,7 @@ export class FrigateCard extends LitElement { view: this._view?.view, fullscreen: screenfull.isEnabled && screenfull.isFullscreen, camera: this._view?.camera, - mediaLoaded: !!this._currentMediaLoadedInfo, + media_loaded: !!this._currentMediaLoadedInfo, ...(this._conditionManager?.hasHAStateConditions && { state: this._hass?.states, }), diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts index 4f8b71c8..ab2ebd21 100644 --- a/src/config-mgmt.ts +++ b/src/config-mgmt.ts @@ -1,4 +1,4 @@ -import cloneDeep from 'lodash-es/cloneDeep' +import cloneDeep from 'lodash-es/cloneDeep'; import get from 'lodash-es/get'; import isEqual from 'lodash-es/isEqual'; import set from 'lodash-es/set'; @@ -6,6 +6,7 @@ import { CONF_CAMERAS, CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_LIVE_PROVIDER, + CONF_ELEMENTS, CONF_IMAGE_URL, CONF_LIVE_AUTO_UNMUTE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, @@ -122,7 +123,7 @@ export const trimConfig = function (obj: RawFrigateCardConfig): boolean { for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (typeof obj[key] === 'object' && obj[key] != null) { - modified ||= trimConfig(obj[key] as RawFrigateCardConfig); + modified = trimConfig(obj[key] as RawFrigateCardConfig) || modified; if (!Object.keys(obj[key] as RawFrigateCardConfig).length) { delete obj[key]; @@ -547,6 +548,56 @@ const upgradeThumbnailShowControlsToIndividualControls = ( }; }; +/** + * Recursively upgrade an object. + * @param transform A transform applied to each object recursively. + * @param getObject A function to get the object to be upgraded. + * @returns An upgrade function. + */ +const recursiveUpgradeObject = ( + transform: (data: RawFrigateCardConfig) => boolean, + getObject?: (data: RawFrigateCardConfig) => RawFrigateCardConfig | undefined | null, +): ((data: RawFrigateCardConfig) => boolean) => { + const recurse = (data: RawFrigateCardConfig): boolean => { + let result = false; + if (data && typeof data === 'object') { + const object = getObject ? getObject(data) : data; + if (object) { + result = transform(object) || result; + } + if (Array.isArray(data)) { + data + .filter((item) => typeof item === 'object') + .forEach((item: RawFrigateCardConfig) => { + result = recurse(item) || result; + }); + } else { + Object.keys(data) + .filter((key) => typeof data[key] === 'object') + .forEach((key) => { + result = recurse(data[key] as RawFrigateCardConfig) || result; + }); + } + } + return result; + }; + return recurse; +}; + +/** + * Transform mediaLoaded -> media_loaded + * @param data Input data. + * @returns `true` if the configuration was modified. + */ +const transformConditionMediaLoaded = (data: unknown): boolean => { + if (typeof data === 'object' && data && data['mediaLoaded'] !== undefined) { + data['media_loaded'] = data['mediaLoaded']; + delete data['mediaLoaded']; + return true; + } + return false; +}; + const UPGRADES = [ // v1.2.1 -> v2.0.0 upgradeMoveTo('frigate_url', 'frigate.url'), @@ -645,4 +696,17 @@ const UPGRADES = [ upgradeThumbnailShowControlsToIndividualControls('media_viewer.controls.thumbnails'), upgradeThumbnailShowControlsToIndividualControls('live.controls.thumbnails'), upgradeThumbnailShowControlsToIndividualControls('timeline.controls.thumbnails'), + + // v4.0.0 -> v4.1.0 + upgradeArrayValue( + CONF_OVERRIDES, + transformConditionMediaLoaded, + (data) => data.conditions as RawFrigateCardConfig | undefined, + ), + (data: unknown): boolean => { + return recursiveUpgradeObject( + transformConditionMediaLoaded, + (data) => data.conditions as RawFrigateCardConfig | undefined, + )(typeof data === 'object' && data ? data[CONF_ELEMENTS] : {}); + }, ]; diff --git a/src/const.ts b/src/const.ts index b0db79f8..2a2d3504 100644 --- a/src/const.ts +++ b/src/const.ts @@ -33,6 +33,8 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY = export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES = `${CONF_CAMERAS}.#.triggers.entities` as const; +export const CONF_ELEMENTS = 'elements' as const; + export const CONF_VIEW = 'view' as const; export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const; export const CONF_VIEW_DARK_MODE = `${CONF_VIEW}.dark_mode` as const; diff --git a/src/types.ts b/src/types.ts index 77d58d28..fd2a01e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -500,7 +500,7 @@ const frigateCardConditionSchema = z.object({ view: z.string().array().optional(), fullscreen: z.boolean().optional(), camera: z.string().array().optional(), - mediaLoaded: z.boolean().optional(), + media_loaded: z.boolean().optional(), state: stateConditions.optional(), media_query: z.string().optional(), });