From c7506c9ee0f79178e4b6f59ea26d3c76ff35d786 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 14 Nov 2021 15:32:25 -0800 Subject: [PATCH] Add menu conditions. --- README.md | 524 +++++++++++++++++--------------- src/action-handler-directive.ts | 2 +- src/card-condition.ts | 68 +++++ src/card.ts | 37 ++- src/components/elements.ts | 135 +++++--- src/components/menu.ts | 6 +- src/types.ts | 11 +- 7 files changed, 481 insertions(+), 302 deletions(-) create mode 100644 src/card-condition.ts diff --git a/README.md b/README.md index 629cfc25..ed7a2784 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ All variables listed are under a `menu:` section. | `mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.| | `button_size` | `40px` | The size of the menu buttons (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].| | `buttons.{frigate, live, clips, snapshots, image, download, frigate_ui, fullscreen}` | `true`, except for `image` | Whether or not to show these builtin actions in the card menu. | +| `conditions` | | Condition(s) that must be met in order for the menu to be displayed. These conditions use the same format as the `custom:frigate-card-conditional` card (see [Possible conditions](#frigate-card-conditions) below). If conditions are specified but not met, then the menu is not rendered.| ### Live options @@ -193,16 +194,6 @@ The card aspect ratio can be changed with the `dimensions.aspect_ratio_mode` and If no aspect ratio is specified or available, but one is needed then `16:9` will be used by default. -#### Example aspect ratio configuration - -Have the card aspect-ratio dynamically follow the last loaded media, but use `4:3` as the default when there is no such media: - -```yaml -dimensions: - aspect_ratio_mode: dynamic - aspect_ratio: '4:3' -``` - ### Other Options @@ -289,9 +280,19 @@ Parameters for the `custom:frigate-card-conditional` element: | Parameter | Description | | ------------- | --------------------------------------------- | | `type` | Must be `custom:frigate-card-conditional`. | -| `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. | -| `conditions.view` | A list of [views](#views) in which these elements should be rendered. | -| `elements` | The elements to render. Can be any supported element, include additional condition or custom elements. | +| `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. See below. | + `elements` | The elements to render. Can be any supported element, include additional condition or custom elements. | + + + +##### Frigate Card Conditions + +All variables listed are under a `conditions:` section. + +| Condition | Description | +| ------------- | --------------------------------------------- | +| `view` | A list of [views](#views) in which these elements should be rendered. | +| `fullscreen` | If `true` the elements are only rendered if the card is in fullscreen mode. If `false` the elements are only rendered if the card is **NOT** in fullscreen mode.| See the [PTZ example below](#frigate-card-conditional-example) for a real-world example. @@ -311,188 +312,6 @@ See the [PTZ example below](#frigate-card-conditional-example) for a real-world |`frigate_ui`|Open the Frigate UI at the configured URL.| |`fullscreen`|Toggle fullscreen.| -### Elements Examples - -#### Menu icons - -You can add custom icons to the menu with arbitrary actions. - -
- Expand: Custom menu icon - -This example adds an icon that navigates the browser to the releases page for this -card: - -```yaml - - type: custom:frigate-card-menu-icon - icon: mdi:book - tap_action: - action: url - url_path: https://github.com/dermotduffy/frigate-hass-card/releases -``` -
- -#### Menu state icons - -You can add custom state icons to the menu to show the state of an entity and complete arbitrary actions. - -
- Expand: Custom menu state icon - -This example adds an icon that represents the state of the -`light.office_main_lights` entity, that toggles the light on double click. - -```yaml -elements: - - type: custom:frigate-card-menu-state-icon - entity: light.office_main_lights - tap_action: - action: toggle -``` -
- -#### State badges - -You can adds a state badge to the card showing arbitrary entity states. - -
- Expand: State badge - -This example adds a state badge showing the temperature and hides the label text: - -```yaml - - type: state-badge - entity: sensor.kitchen_temperature - style: - right: '-20px' - top: 100px - color: rgba(0,0,0,0) - opacity: 0.5 -``` - -Picture elements temperature example -
- -#### Conditional menu icons - -You can have icons conditionally added to the menu based on entity state. - -
- Expand: Conditional menu icons - -This example only adds the light entity to the menu if a light is on. - -```yaml - - type: conditional - conditions: - - entity: light.kitchen - state: 'on' - elements: - - type: custom:frigate-card-menu-state-icon - entity: light.kitchen - tap_action: - action: toggle -``` -
- - - -#### Restricting icons to certain views - -You can restrict icons to only show for certain [views](#views) using a -`custom:frigate-card-conditional` element (e.g. PTZ controls) - -
- Expand: View-based conditions (e.g. PTZ controls) - -This example shows PTZ icons that call a PTZ service, but only in the `live` view. - -```yaml -elements: - - type: custom:frigate-card-conditional - conditions: - view: - - live - elements: - - type: icon - icon: mdi:arrow-up - style: - background: rgba(255, 255, 255, 0.25) - border-radius: 5px - right: 25px - bottom: 50px - tap_action: - action: call-service - service: amcrest.ptz_control - service_data: - entity_id: camera.kitchen - movement: up - - type: icon - icon: mdi:arrow-down - style: - background: rgba(255, 255, 255, 0.25) - border-radius: 5px - right: 25px - bottom: 0px - tap_action: - action: call-service - service: amcrest.ptz_control - service_data: - entity_id: camera.kitchen - movement: down - - type: icon - icon: mdi:arrow-left - style: - background: rgba(255, 255, 255, 0.25) - border-radius: 5px - right: 50px - bottom: 25px - tap_action: - action: call-service - service: amcrest.ptz_control - service_data: - entity_id: camera.kitchen - movement: left - - type: icon - icon: mdi:arrow-right - style: - background: rgba(255, 255, 255, 0.25) - border-radius: 5px - right: 0px - bottom: 25px - tap_action: - action: call-service - service: amcrest.ptz_control - service_data: - entity_id: camera.kitchen - movement: right -``` -
- - - -#### Triggering card actions - -You can control the card itself with the `custom:frigate-card-action` action. - -
- Expand: Custom fullscreen button - -This example shows an icon that toggles the card fullscreen mode. - -```yaml -elements: - - type: icon - icon: mdi:fullscreen - style: - left: 40px - top: 40px - tap_action: - action: custom:frigate-card-action - frigate_card_action: fullscreen -``` - -
@@ -562,25 +381,6 @@ _also_, which means that a card-wide `tap` action probably isn't that useful as it may be disorienting to the user and will trigger on all kinds of basic interaction on the card (e.g. tapping/clicking a menu button). -### Example - -In this example, double clicking the card in any view will cause the card to go -into fullscreen mode, **except** when the view is `live` in which case the -office lights are toggled. - -```yaml -view: - actions: - double_tap_action: - action: custom:frigate-card-action - frigate_card_action: fullscreen -live: - provider: frigate-jsmpeg - actions: - entity: light.office_main_lights - double_tap_action: - action: toggle -``` ## Menu Modes @@ -595,30 +395,6 @@ This card supports several menu configurations. |`below`| Render the menu below the card. The 'F' button shows the default view. | Menu below | |`none`| No menu is shown. | No Menu | - - -## Example YAML Configuration - -A configuration that uses WebRTC for live: - -```yaml -- type: 'custom:frigate-card' - camera_entity: camera.front_door - frigate_url: http://frigate - live_provider: webrtc - webrtc: - entity: camera.front_door_rtsp -``` - -A configuration that shows the latest clip on load, but does not automatically play it: - -```yaml -- type: 'custom:frigate-card' - camera_entity: camera.front_door - frigate_url: http://frigate - view_default: clip -``` - ## Screenshot: Snapshot / Clip Gallery Full viewing of clips: @@ -631,6 +407,278 @@ This card supports full editing via the Lovelace card editor. Additional arbitra Live viewing +## Examples + +### WebRTC + +
+ Expand: Basic WebRTC configuration + +```yaml +type: 'custom:frigate-card' +camera_entity: camera.front_door +live: + provider: webrtc + webrtc: + entity: camera.front_door_rtsp +``` + +
+ +### Static Aspect Ratios + +You can set a static aspect ratio. + +
+ Expand: Static 4:3 aspect ratios + +```yaml +[...] +dimensions: + aspect_ratio_mode: dynamic + aspect_ratio: '4:3' +``` +
+ +### Adding Menu Icons + +You can add custom icons to the menu with arbitrary actions. + +
+ Expand: Custom menu icon + +This example adds an icon that navigates the browser to the releases page for this +card: + +```yaml +[...] +elements: + - type: custom:frigate-card-menu-icon + icon: mdi:book + tap_action: + action: url + url_path: https://github.com/dermotduffy/frigate-hass-card/releases +``` +
+ +### Adding Menu State Icons + +You can add custom state icons to the menu to show the state of an entity and complete arbitrary actions. + +
+ Expand: Custom menu state icon + +This example adds an icon that represents the state of the +`light.office_main_lights` entity, that toggles the light on double click. + +```yaml +[...] +elements: + - type: custom:frigate-card-menu-state-icon + entity: light.office_main_lights + tap_action: + action: toggle +``` +
+ +### Adding State Badges + +You can adds a state badge to the card showing arbitrary entity states. + +
+ Expand: State badge + +This example adds a state badge showing the temperature and hides the label text: + +```yaml +[...] +elements: + - type: state-badge + entity: sensor.kitchen_temperature + style: + right: '-20px' + top: 100px + color: rgba(0,0,0,0) + opacity: 0.5 +``` + +Picture elements temperature example +
+ +### Adding State Badges + +You can have icons conditionally added to the menu based on entity state. + +
+ Expand: Conditional menu icons + +This example only adds the light entity to the menu if a light is on. + +```yaml +[...] +elements: + - type: conditional + conditions: + - entity: light.kitchen + state: 'on' + elements: + - type: custom:frigate-card-menu-state-icon + entity: light.kitchen + tap_action: + action: toggle +``` +
+ + + +### Restricting icons to certain views + +You can restrict icons to only show for certain [views](#views) using a +`custom:frigate-card-conditional` element (e.g. PTZ controls) + +
+ Expand: View-based conditions (e.g. PTZ controls) + +This example shows PTZ icons that call a PTZ service, but only in the `live` view. + +```yaml +[...] +elements: + - type: custom:frigate-card-conditional + conditions: + view: + - live + elements: + - type: icon + icon: mdi:arrow-up + style: + background: rgba(255, 255, 255, 0.25) + border-radius: 5px + right: 25px + bottom: 50px + tap_action: + action: call-service + service: amcrest.ptz_control + service_data: + entity_id: camera.kitchen + movement: up + - type: icon + icon: mdi:arrow-down + style: + background: rgba(255, 255, 255, 0.25) + border-radius: 5px + right: 25px + bottom: 0px + tap_action: + action: call-service + service: amcrest.ptz_control + service_data: + entity_id: camera.kitchen + movement: down + - type: icon + icon: mdi:arrow-left + style: + background: rgba(255, 255, 255, 0.25) + border-radius: 5px + right: 50px + bottom: 25px + tap_action: + action: call-service + service: amcrest.ptz_control + service_data: + entity_id: camera.kitchen + movement: left + - type: icon + icon: mdi:arrow-right + style: + background: rgba(255, 255, 255, 0.25) + border-radius: 5px + right: 0px + bottom: 25px + tap_action: + action: call-service + service: amcrest.ptz_control + service_data: + entity_id: camera.kitchen + movement: right +``` +
+ + + +### Triggering card actions + +You can control the card itself with the `custom:frigate-card-action` action. + +
+ Expand: Custom fullscreen button + +This example shows an icon that toggles the card fullscreen mode. + +```yaml +[...] +elements: + - type: icon + icon: mdi:fullscreen + style: + left: 40px + top: 40px + tap_action: + action: custom:frigate-card-action + frigate_card_action: fullscreen +``` +
+ +### Adding card-wide actions + +You can add actions to the card to be trigger on `tap`, `double_tap` or `hold`. See [actions](#actions) above. + +
+ Expand: Adding a card-wide action + +In this example double clicking the card in any view will cause the card to go +into fullscreen mode, **except** when the view is `live` in which case the +office lights are toggled. + +```yaml +[...] +view: + actions: + double_tap_action: + action: custom:frigate-card-action + frigate_card_action: fullscreen +live: + provider: frigate-jsmpeg + actions: + entity: light.office_main_lights + double_tap_action: + action: toggle +``` +
+ +### Hiding the menu in certain circumstances + +You can add conditions to the menu, which will hide the menu unless met. + +
+ Expand: Hiding the menu + +This example hides the menu unless the card is in fullscreen mode, and uses a card-wide action to enable fullscreen mode on `double_tap`: + +```yaml +[...] +view: + actions: + double_tap_action: + action: custom:frigate-card-action + frigate_card_action: fullscreen +menu: + conditions: + fullscreen: true +``` + +
+ ## Troubleshooting diff --git a/src/action-handler-directive.ts b/src/action-handler-directive.ts index 0d330cae..68403241 100644 --- a/src/action-handler-directive.ts +++ b/src/action-handler-directive.ts @@ -25,7 +25,7 @@ declare global { } class ActionHandler extends HTMLElement implements ActionHandler { - public holdTime = 500; + public holdTime = 400; // eslint-disable-next-line @typescript-eslint/no-explicit-any public ripple: any; diff --git a/src/card-condition.ts b/src/card-condition.ts new file mode 100644 index 00000000..8574d9bf --- /dev/null +++ b/src/card-condition.ts @@ -0,0 +1,68 @@ +import type { FrigateCardCondition } from './types'; +import { View } from './view'; + +export interface ConditionState { + view?: View; + fullscreen?: boolean; +} + +class ConditionStateRequestEvent extends Event { + public conditionState?: ConditionState; +} + +export function evaluateCondition( + condition?: FrigateCardCondition, + state?: ConditionState, +): boolean { + let result = true; + if (condition?.view?.length && state?.view) { + result &&= condition?.view.includes(state?.view.view); + } + if (condition?.fullscreen !== undefined && state?.fullscreen !== undefined) { + result &&= condition?.fullscreen == state?.fullscreen; + } + return result; +} + +/** + * Evaluate whether a frigateCardCondition is met using an event to fetch state. + * @returns A boolean indicating whether the condition is met. + */ +export function fetchStateAndEvaluateCondition( + node: HTMLElement, + condition?: FrigateCardCondition, +): boolean { + if (!condition) { + return true; + } + + const stateEvent = new ConditionStateRequestEvent( + `frigate-card:condition-state-request`, + { + bubbles: true, + composed: true, + }, + ); + + /* Special note on what's going on here: + * + * Some parts of the card (e.g. ) may have arbitrary + * complexity and layers (that this card doesn't control) between that master + * element and the element that needs to evaluate the condition. In these + * cases there's no clean way to pass state from the rest of card down + * through these layers. Instead, an event is dispatched as a "request for + * state" (StateRequestEvent) upwards which is caught by the outer card + * and state added to the event object. Because event propagation is handled + * synchronously, the state will be added to the event before the flow + * proceeds. + */ + node.dispatchEvent(stateEvent); + return evaluateCondition(condition, stateEvent.conditionState); +} + +export function conditionStateRequestHandler( + ev: ConditionStateRequestEvent, + conditionState?: ConditionState, +): void { + ev.conditionState = conditionState; +} diff --git a/src/card.ts b/src/card.ts index 270c3836..d23b86fd 100644 --- a/src/card.ts +++ b/src/card.ts @@ -12,7 +12,6 @@ import { classMap } from 'lit/directives/class-map.js'; import { styleMap } from 'lit/directives/style-map.js'; import { until } from 'lit/directives/until.js'; import { - ActionConfig, HomeAssistant, LovelaceCardEditor, getLovelace, @@ -76,6 +75,7 @@ import { ResolvedMediaCache } from './resolved-media.js'; import { BrowseMediaUtil } from './browse-media-util.js'; import { isConfigUpgradeable } from './config-mgmt.js'; import { actionHandler } from './action-handler-directive.js'; +import { ConditionState, conditionStateRequestHandler } from './card-condition.js'; /** A note on media callbacks: * @@ -136,6 +136,9 @@ export class FrigateCard extends LitElement { @property({ attribute: false }) protected _view: View = new View(); + @state() + protected _conditionState?: ConditionState; + @query('frigate-card-menu') _menu!: FrigateCardMenu; @@ -212,6 +215,16 @@ export class FrigateCard extends LitElement { } as FrigateCardConfig; } + /** + * Generate the state used to evaluate conditions. + */ + protected _generateConditionState(): void { + this._conditionState = { + view: this._view, + fullscreen: screenfull.isEnabled && screenfull.isFullscreen, + }; + } + /** * Get a FrigateCard MenuButton given a set of parameters. * @param params Menu button parameters. @@ -500,7 +513,7 @@ export class FrigateCard extends LitElement { this._changeView(); } - protected _changeView(view?: View | undefined): void { + protected _changeView(view?: View): void { this._message = null; if (view === undefined) { @@ -508,6 +521,7 @@ export class FrigateCard extends LitElement { } else { this._view = view; } + this._generateConditionState(); } /** @@ -719,6 +733,7 @@ export class FrigateCard extends LitElement { .hass=${this._hass} .menuConfig=${this.config.menu} .buttons=${this._getMenuButtons()} + .conditionState=${this._conditionState} class="${classMap(classes)}" > `; @@ -811,6 +826,7 @@ export class FrigateCard extends LitElement { * Handler called when fullscreen is toggled. */ protected _fullScreenHandler(): void { + this._generateConditionState(); // Re-render after a change to fullscreen mode to take advantage of // the expanded screen real-estate (vs staying in aspect-ratio locked // modes). @@ -889,11 +905,11 @@ export class FrigateCard extends LitElement { if (this._view.is('live')) { specificActions = this.config.live.actions; } else if (this._view.isGalleryView()) { - specificActions = this.config.event_gallery?.actions; + specificActions = this.config.event_gallery?.actions; } else if (this._view.isViewerView()) { - specificActions = this.config.event_viewer.actions; + specificActions = this.config.event_viewer.actions; } else if (this._view.is('image')) { - specificActions = this.config.image?.actions; + specificActions = this.config.image?.actions; } return { ...this.config.view.actions, ...specificActions }; } @@ -951,8 +967,7 @@ export class FrigateCard extends LitElement { hasHold: hasAction(actions.hold_action), hasDoubleClick: hasAction(actions.double_tap_action), })} - @action=${(ev: CustomEvent) => - this._actionHandler(ev, actions)} + @action=${(ev: CustomEvent) => this._actionHandler(ev, actions)} @ll-custom=${this._cardActionHandler.bind(this)} > ${this.config.menu.mode == 'above' ? this._renderMenu() : ''} @@ -1072,7 +1087,7 @@ export class FrigateCard extends LitElement { { this._addDynamicMenuButton(e.detail); @@ -1080,10 +1095,8 @@ export class FrigateCard extends LitElement { @frigate-card:menu-remove=${(e) => { this._removeDynamicMenuButton(e.detail); }} - @frigate-card:state-request=${(e) => { - // State filled here must also trigger the - // 'frigate-card-elements' to re-render (by being a property). - e.view = this._view; + @frigate-card:condition-state-request=${(ev) => { + conditionStateRequestHandler(ev, this._conditionState) }} > diff --git a/src/components/elements.ts b/src/components/elements.ts index b92ca112..4422b4dd 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -17,7 +17,7 @@ import { import elementsStyle from '../scss/elements.scss'; import { localize } from '../localize/localize.js'; -import { View } from '../view.js'; +import { ConditionState, fetchStateAndEvaluateCondition } from '../card-condition.js'; /* A note on picture element rendering: * @@ -59,12 +59,19 @@ class FrigateCardElementsCore extends LitElement { @property({ attribute: false }) protected elements: PictureElements; + /** + * Need to ensure card re-renders when conditionState changes, hence having it + * as a property even though it is not currently directly used by this class. + */ @property({ attribute: false }) - protected view?: View; + protected conditionState?: ConditionState; protected _root: HTMLElement | null = null; - protected _hass!: HomeAssistant & ExtendedHomeAssistant; + protected _hass?: HomeAssistant & ExtendedHomeAssistant; + /** + * Set Home Assistant object. + */ set hass(hass: HomeAssistant & ExtendedHomeAssistant) { if (this._root) { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -73,15 +80,21 @@ class FrigateCardElementsCore extends LitElement { this._hass = hass; } - // Transparent to elements. + /** + * Create a transparent render root. + */ createRenderRoot(): LitElement { return this; } + /** + * Create the root node for our picture elements. + * @returns + */ protected _createRoot(): HTMLElement { // eslint-disable-next-line @typescript-eslint/no-explicit-any const elementConstructor = customElements.get('hui-conditional-element') as any; - if (!elementConstructor) { + if (!elementConstructor || !this._hass) { throw new Error(localize('error.could_not_render_elements')); } @@ -101,6 +114,10 @@ class FrigateCardElementsCore extends LitElement { return element; } + /** + * Render the elements. + * @returns A rendered template or void. + */ protected render(): TemplateResult | void { try { // Recreate the root on each render to ensure conditional ancestors @@ -113,20 +130,25 @@ class FrigateCardElementsCore extends LitElement { } } -// THe master class, handles event listeners and styles. +/** + * The master class, handles event listeners and styles. + */ @customElement('frigate-card-elements') export class FrigateCardElements extends LitElement { @property({ attribute: false }) protected elements: PictureElements; @property({ attribute: false }) - protected view!: View; + protected conditionState?: ConditionState; - protected _hass!: HomeAssistant & ExtendedHomeAssistant; + protected _hass?: HomeAssistant & ExtendedHomeAssistant; @query('frigate-card-elements-core') _core!: FrigateCardElementsCore; + /** + * Set the Home Assistant object. + */ set hass(hass: HomeAssistant & ExtendedHomeAssistant) { if (this._core) { this._core.hass = hass; @@ -134,6 +156,10 @@ export class FrigateCardElements extends LitElement { this._hass = hass; } + /** + * Handle a picture element to be removed from the menu. + * @param ev The event. + */ protected _menuRemoveHandler(ev: Event): void { // Re-dispatch event from this element (instead of the disconnected one, as // there is no parent of the disconnected element). @@ -144,6 +170,10 @@ export class FrigateCardElements extends LitElement { ); } + /** + * Handle a picture element to be added to the menu. + * @param ev The event. + */ protected _menuAddHandler(ev: Event): void { ev = ev as CustomEvent; const path = ev.composedPath(); @@ -165,6 +195,9 @@ export class FrigateCardElements extends LitElement { ); } + /** + * Connected callback. + */ connectedCallback(): void { super.connectedCallback(); @@ -173,40 +206,51 @@ export class FrigateCardElements extends LitElement { this.addEventListener('frigate-card:menu-add', this._menuAddHandler); } + /** + * Disconnected callback. + */ disconnectedCallback(): void { this.removeEventListener('frigate-card:menu-add', this._menuAddHandler); super.disconnectedCallback(); } + /** + * Render the template. + * @returns A rendered template. + */ protected render(): TemplateResult { return html` `; } + /** + * Get styles. + */ static get styles(): CSSResultGroup { return unsafeCSS(elementsStyle); } } -class StateRequestEvent extends Event { - public view: View | undefined; -} - -// An element that can render others based on Frigate state (e.g. only show -// overlays in particular views). This is the Frigate Card equivalent to the HA -// conditional card. +/** + * An element that can render others based on Frigate state (e.g. only show + * overlays in particular views). This is the Frigate Card equivalent to the HA + * conditional card. + */ @customElement('frigate-card-conditional') export class FrigateCardElementsConditional extends LitElement { - protected _config: FrigateConditional | null = null; - protected _hass!: HomeAssistant & ExtendedHomeAssistant; + protected _config?: FrigateConditional; + protected _hass?: HomeAssistant & ExtendedHomeAssistant; @query('frigate-card-elements-core') - _core!: FrigateCardElementsCore; + _core?: FrigateCardElementsCore; + /** + * Set the Home Assistant object. + */ set hass(hass: HomeAssistant & ExtendedHomeAssistant) { if (this._core) { this._core.hass = hass; @@ -214,22 +258,25 @@ export class FrigateCardElementsConditional extends LitElement { this._hass = hass; } + /** + * Set the card configuration. + * @param config The card configuration. + */ public setConfig(config: FrigateConditional): void { this._config = config; } - // Transparent to elements. + /** + * Create a root into which to render. This card is "transparent". + * @returns + */ createRenderRoot(): LitElement { return this; } - protected evaluate(stateEvent: StateRequestEvent): boolean { - if (stateEvent.view && this._config.conditions.view) { - return this._config.conditions.view.includes(stateEvent.view.view); - } - return true; - } - + /** + * Connected callback. + */ connectedCallback(): void { super.connectedCallback(); @@ -239,27 +286,11 @@ export class FrigateCardElementsConditional extends LitElement { this.className = ''; } + /** + * Render the card. + */ protected render(): TemplateResult | void { - const stateEvent = new StateRequestEvent(`frigate-card:state-request`, { - bubbles: true, - composed: true, - }); - - /* Special note on what's going on here: - * - * Picture elements all are descendents of , but - * there may be arbitrary complexity and layers (that this card doesn't - * control) between that master element and this custom conditional element. - * This element needs Frigate card state to function (e.g. view), but - * there's no clean way to pass state from the rest of card down through - * these layers. Instead, we dispatch a "request for state" - * (StateRequestEvent) event upwards which is caught by the outer card and - * state added to the event object. Because event propagation is handled - * synchronously, the state will be added to the event before the flow - * proceeds. - */ - this.dispatchEvent(stateEvent); - if (this.evaluate(stateEvent)) { + if (fetchStateAndEvaluateCondition(this, this._config.conditions)) { return html` extends LitElement { @property({ attribute: false }) protected _config: T | null = null; + /** + * Set the card config. + * @param config The configuration. + */ public setConfig(config: T): void { this._config = config; } + /** + * Connected callback. + */ connectedCallback(): void { super.connectedCallback(); if (this._config) { @@ -285,6 +323,9 @@ export class FrigateCardElementsBaseMenuIcon extends LitElement { } } + /** + * Disconnected callback. + */ disconnectedCallback(): void { if (this._config) { dispatchFrigateCardEvent(this, 'menu-remove', this._config); diff --git a/src/components/menu.ts b/src/components/menu.ts index a3468bb4..29625e4e 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -26,6 +26,7 @@ import { } from '../common.js'; import menuStyle from '../scss/menu.scss'; +import { ConditionState, evaluateCondition } from '../card-condition.js'; export const MENU_HEIGHT = 46; export const FRIGATE_BUTTON_MENU_ICON = 'frigate'; @@ -53,6 +54,9 @@ export class FrigateCardMenu extends LitElement { @property({ attribute: false }) public buttons: MenuButton[] = []; + @property({ attribute: false }) + protected conditionState?: ConditionState; + /** * Handle an action on a menu button. * @param ev The action event. @@ -196,7 +200,7 @@ export class FrigateCardMenu extends LitElement { } const mode = this._menuConfig.mode; - if (mode == 'none') { + if (mode == 'none' || !evaluateCondition(this._menuConfig.conditions, this.conditionState)) { return; } diff --git a/src/types.ts b/src/types.ts index 2fdc2182..3c9c78c3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -267,11 +267,15 @@ export const menuStateIconSchema = stateIconSchema.merge( ); export type MenuStateIcon = z.infer; +const frigateCardConditionSchema = z.object({ + view: z.string().array().optional(), + fullscreen: z.boolean().optional(), +}); +export type FrigateCardCondition = z.infer; + const frigateConditionalSchema = z.object({ type: z.literal('custom:frigate-card-conditional'), - conditions: z.object({ - view: z.string().array().optional(), - }), + conditions: frigateCardConditionSchema, elements: z.lazy(() => pictureElementsSchema), }); export type FrigateConditional = z.infer; @@ -407,6 +411,7 @@ const menuConfigSchema = z }) .default(menuConfigDefault.buttons), button_size: z.string().default(menuConfigDefault.button_size), + conditions: frigateCardConditionSchema.optional(), }) .default(menuConfigDefault); export type MenuConfig = z.infer;