Add menu conditions.

This commit is contained in:
Dermot Duffy
2021-11-14 15:32:25 -08:00
parent 3ae908d4b7
commit c7506c9ee0
7 changed files with 481 additions and 302 deletions
+286 -238
View File
@@ -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.| | `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].| | `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. | | `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 ### 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 If no aspect ratio is specified or available, but one is needed then `16:9` will
be used by default. 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'
```
<a name="other-options"></a> <a name="other-options"></a>
### Other Options ### Other Options
@@ -289,9 +280,19 @@ Parameters for the `custom:frigate-card-conditional` element:
| Parameter | Description | | Parameter | Description |
| ------------- | --------------------------------------------- | | ------------- | --------------------------------------------- |
| `type` | Must be `custom:frigate-card-conditional`. | | `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` | A set of conditions that must evaluate to true in order for the elements to be rendered. See below. |
| `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. |
| `elements` | The elements to render. Can be any supported element, include additional condition or custom elements. |
<a name="frigate-card-conditions"></a>
##### 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. 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.| |`frigate_ui`|Open the Frigate UI at the configured URL.|
|`fullscreen`|Toggle fullscreen.| |`fullscreen`|Toggle fullscreen.|
### Elements Examples
#### Menu icons
You can add custom icons to the menu with arbitrary actions.
<details>
<summary>Expand: Custom menu icon</summary>
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
```
</details>
#### Menu state icons
You can add custom state icons to the menu to show the state of an entity and complete arbitrary actions.
<details>
<summary>Expand: Custom menu state icon</summary>
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
```
</details>
#### State badges
You can adds a state badge to the card showing arbitrary entity states.
<details>
<summary>Expand: State badge</summary>
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
```
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/picture_elements_temperature.png" alt="Picture elements temperature example" width="400px">
</details>
#### Conditional menu icons
You can have icons conditionally added to the menu based on entity state.
<details>
<summary>Expand: Conditional menu icons</summary>
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
```
</details>
<a name="frigate-card-conditional-example"></a>
#### 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)
<details>
<summary>Expand: View-based conditions (e.g. PTZ controls)</summary>
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
```
</details>
<a name="frigate-card-action"></a>
#### Triggering card actions
You can control the card itself with the `custom:frigate-card-action` action.
<details>
<summary>Expand: Custom fullscreen button</summary>
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
```
</details>
<a name="views"></a> <a name="views"></a>
@@ -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 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). 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 ## 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. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-below.png" alt="Menu below" width="400px"> | |`below`| Render the menu below the card. The 'F' button shows the default view. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-below.png" alt="Menu below" width="400px"> |
|`none`| No menu is shown. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-none.png" alt="No Menu" width="400px"> | |`none`| No menu is shown. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-none.png" alt="No Menu" width="400px"> |
<a name="yaml-examples"></a>
## 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 ## Screenshot: Snapshot / Clip Gallery
Full viewing of clips: Full viewing of clips:
@@ -631,6 +407,278 @@ This card supports full editing via the Lovelace card editor. Additional arbitra
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/editor.png" alt="Live viewing" width="400px"> <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/editor.png" alt="Live viewing" width="400px">
## Examples
### WebRTC
<details>
<summary>Expand: Basic WebRTC configuration</summary>
```yaml
type: 'custom:frigate-card'
camera_entity: camera.front_door
live:
provider: webrtc
webrtc:
entity: camera.front_door_rtsp
```
</details>
### Static Aspect Ratios
You can set a static aspect ratio.
<details>
<summary>Expand: Static 4:3 aspect ratios</summary>
```yaml
[...]
dimensions:
aspect_ratio_mode: dynamic
aspect_ratio: '4:3'
```
</details>
### Adding Menu Icons
You can add custom icons to the menu with arbitrary actions.
<details>
<summary>Expand: Custom menu icon</summary>
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
```
</details>
### Adding Menu State Icons
You can add custom state icons to the menu to show the state of an entity and complete arbitrary actions.
<details>
<summary>Expand: Custom menu state icon</summary>
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
```
</details>
### Adding State Badges
You can adds a state badge to the card showing arbitrary entity states.
<details>
<summary>Expand: State badge</summary>
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
```
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/picture_elements_temperature.png" alt="Picture elements temperature example" width="400px">
</details>
### Adding State Badges
You can have icons conditionally added to the menu based on entity state.
<details>
<summary>Expand: Conditional menu icons</summary>
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
```
</details>
<a name="frigate-card-conditional-example"></a>
### 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)
<details>
<summary>Expand: View-based conditions (e.g. PTZ controls)</summary>
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
```
</details>
<a name="frigate-card-action"></a>
### Triggering card actions
You can control the card itself with the `custom:frigate-card-action` action.
<details>
<summary>Expand: Custom fullscreen button</summary>
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
```
</details>
### Adding card-wide actions
You can add actions to the card to be trigger on `tap`, `double_tap` or `hold`. See [actions](#actions) above.
<details>
<summary>Expand: Adding a card-wide action</summary>
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
```
</details>
### Hiding the menu in certain circumstances
You can add conditions to the menu, which will hide the menu unless met.
<details>
<summary>Expand: Hiding the menu</summary>
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
```
</details>
## Troubleshooting ## Troubleshooting
<a name="jsmpeg-troubleshooting"></a> <a name="jsmpeg-troubleshooting"></a>
+1 -1
View File
@@ -25,7 +25,7 @@ declare global {
} }
class ActionHandler extends HTMLElement implements ActionHandler { class ActionHandler extends HTMLElement implements ActionHandler {
public holdTime = 500; public holdTime = 400;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public ripple: any; public ripple: any;
+68
View File
@@ -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. <frigate-card-elements>) 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;
}
+22 -9
View File
@@ -12,7 +12,6 @@ import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { until } from 'lit/directives/until.js'; import { until } from 'lit/directives/until.js';
import { import {
ActionConfig,
HomeAssistant, HomeAssistant,
LovelaceCardEditor, LovelaceCardEditor,
getLovelace, getLovelace,
@@ -76,6 +75,7 @@ import { ResolvedMediaCache } from './resolved-media.js';
import { BrowseMediaUtil } from './browse-media-util.js'; import { BrowseMediaUtil } from './browse-media-util.js';
import { isConfigUpgradeable } from './config-mgmt.js'; import { isConfigUpgradeable } from './config-mgmt.js';
import { actionHandler } from './action-handler-directive.js'; import { actionHandler } from './action-handler-directive.js';
import { ConditionState, conditionStateRequestHandler } from './card-condition.js';
/** A note on media callbacks: /** A note on media callbacks:
* *
@@ -136,6 +136,9 @@ export class FrigateCard extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected _view: View = new View(); protected _view: View = new View();
@state()
protected _conditionState?: ConditionState;
@query('frigate-card-menu') @query('frigate-card-menu')
_menu!: FrigateCardMenu; _menu!: FrigateCardMenu;
@@ -212,6 +215,16 @@ export class FrigateCard extends LitElement {
} as FrigateCardConfig; } 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. * Get a FrigateCard MenuButton given a set of parameters.
* @param params Menu button parameters. * @param params Menu button parameters.
@@ -500,7 +513,7 @@ export class FrigateCard extends LitElement {
this._changeView(); this._changeView();
} }
protected _changeView(view?: View | undefined): void { protected _changeView(view?: View): void {
this._message = null; this._message = null;
if (view === undefined) { if (view === undefined) {
@@ -508,6 +521,7 @@ export class FrigateCard extends LitElement {
} else { } else {
this._view = view; this._view = view;
} }
this._generateConditionState();
} }
/** /**
@@ -719,6 +733,7 @@ export class FrigateCard extends LitElement {
.hass=${this._hass} .hass=${this._hass}
.menuConfig=${this.config.menu} .menuConfig=${this.config.menu}
.buttons=${this._getMenuButtons()} .buttons=${this._getMenuButtons()}
.conditionState=${this._conditionState}
class="${classMap(classes)}" class="${classMap(classes)}"
></frigate-card-menu> ></frigate-card-menu>
`; `;
@@ -811,6 +826,7 @@ export class FrigateCard extends LitElement {
* Handler called when fullscreen is toggled. * Handler called when fullscreen is toggled.
*/ */
protected _fullScreenHandler(): void { protected _fullScreenHandler(): void {
this._generateConditionState();
// Re-render after a change to fullscreen mode to take advantage of // Re-render after a change to fullscreen mode to take advantage of
// the expanded screen real-estate (vs staying in aspect-ratio locked // the expanded screen real-estate (vs staying in aspect-ratio locked
// modes). // modes).
@@ -951,8 +967,7 @@ export class FrigateCard extends LitElement {
hasHold: hasAction(actions.hold_action), hasHold: hasAction(actions.hold_action),
hasDoubleClick: hasAction(actions.double_tap_action), hasDoubleClick: hasAction(actions.double_tap_action),
})} })}
@action=${(ev: CustomEvent) => @action=${(ev: CustomEvent) => this._actionHandler(ev, actions)}
this._actionHandler(ev, actions)}
@ll-custom=${this._cardActionHandler.bind(this)} @ll-custom=${this._cardActionHandler.bind(this)}
> >
${this.config.menu.mode == 'above' ? this._renderMenu() : ''} ${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
@@ -1072,7 +1087,7 @@ export class FrigateCard extends LitElement {
<frigate-card-elements <frigate-card-elements
.hass=${this._hass} .hass=${this._hass}
.elements=${this.config.elements} .elements=${this.config.elements}
.view=${this._view} .conditionState=${this._conditionState}
@frigate-card:message=${this._messageHandler} @frigate-card:message=${this._messageHandler}
@frigate-card:menu-add=${(e) => { @frigate-card:menu-add=${(e) => {
this._addDynamicMenuButton(e.detail); this._addDynamicMenuButton(e.detail);
@@ -1080,10 +1095,8 @@ export class FrigateCard extends LitElement {
@frigate-card:menu-remove=${(e) => { @frigate-card:menu-remove=${(e) => {
this._removeDynamicMenuButton(e.detail); this._removeDynamicMenuButton(e.detail);
}} }}
@frigate-card:state-request=${(e) => { @frigate-card:condition-state-request=${(ev) => {
// State filled here must also trigger the conditionStateRequestHandler(ev, this._conditionState)
// 'frigate-card-elements' to re-render (by being a property).
e.view = this._view;
}} }}
> >
</frigate-card-elements> </frigate-card-elements>
+88 -47
View File
@@ -17,7 +17,7 @@ import {
import elementsStyle from '../scss/elements.scss'; import elementsStyle from '../scss/elements.scss';
import { localize } from '../localize/localize.js'; import { localize } from '../localize/localize.js';
import { View } from '../view.js'; import { ConditionState, fetchStateAndEvaluateCondition } from '../card-condition.js';
/* A note on picture element rendering: /* A note on picture element rendering:
* *
@@ -59,12 +59,19 @@ class FrigateCardElementsCore extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected elements: PictureElements; 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 }) @property({ attribute: false })
protected view?: View; protected conditionState?: ConditionState;
protected _root: HTMLElement | null = null; protected _root: HTMLElement | null = null;
protected _hass!: HomeAssistant & ExtendedHomeAssistant; protected _hass?: HomeAssistant & ExtendedHomeAssistant;
/**
* Set Home Assistant object.
*/
set hass(hass: HomeAssistant & ExtendedHomeAssistant) { set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
if (this._root) { if (this._root) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -73,15 +80,21 @@ class FrigateCardElementsCore extends LitElement {
this._hass = hass; this._hass = hass;
} }
// Transparent to elements. /**
* Create a transparent render root.
*/
createRenderRoot(): LitElement { createRenderRoot(): LitElement {
return this; return this;
} }
/**
* Create the root node for our picture elements.
* @returns
*/
protected _createRoot(): HTMLElement { protected _createRoot(): HTMLElement {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const elementConstructor = customElements.get('hui-conditional-element') as 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')); throw new Error(localize('error.could_not_render_elements'));
} }
@@ -101,6 +114,10 @@ class FrigateCardElementsCore extends LitElement {
return element; return element;
} }
/**
* Render the elements.
* @returns A rendered template or void.
*/
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
try { try {
// Recreate the root on each render to ensure conditional ancestors // Recreate the root on each render to ensure conditional ancestors
@@ -113,20 +130,25 @@ class FrigateCardElementsCore extends LitElement {
} }
} }
// THe master <frigate-card-elements> class, handles event listeners and styles. /**
* The master <frigate-card-elements> class, handles event listeners and styles.
*/
@customElement('frigate-card-elements') @customElement('frigate-card-elements')
export class FrigateCardElements extends LitElement { export class FrigateCardElements extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected elements: PictureElements; protected elements: PictureElements;
@property({ attribute: false }) @property({ attribute: false })
protected view!: View; protected conditionState?: ConditionState;
protected _hass!: HomeAssistant & ExtendedHomeAssistant; protected _hass?: HomeAssistant & ExtendedHomeAssistant;
@query('frigate-card-elements-core') @query('frigate-card-elements-core')
_core!: FrigateCardElementsCore; _core!: FrigateCardElementsCore;
/**
* Set the Home Assistant object.
*/
set hass(hass: HomeAssistant & ExtendedHomeAssistant) { set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
if (this._core) { if (this._core) {
this._core.hass = hass; this._core.hass = hass;
@@ -134,6 +156,10 @@ export class FrigateCardElements extends LitElement {
this._hass = hass; this._hass = hass;
} }
/**
* Handle a picture element to be removed from the menu.
* @param ev The event.
*/
protected _menuRemoveHandler(ev: Event): void { protected _menuRemoveHandler(ev: Event): void {
// Re-dispatch event from this element (instead of the disconnected one, as // Re-dispatch event from this element (instead of the disconnected one, as
// there is no parent of the disconnected element). // 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 { protected _menuAddHandler(ev: Event): void {
ev = ev as CustomEvent<MenuButton>; ev = ev as CustomEvent<MenuButton>;
const path = ev.composedPath(); const path = ev.composedPath();
@@ -165,6 +195,9 @@ export class FrigateCardElements extends LitElement {
); );
} }
/**
* Connected callback.
*/
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
@@ -173,40 +206,51 @@ export class FrigateCardElements extends LitElement {
this.addEventListener('frigate-card:menu-add', this._menuAddHandler); this.addEventListener('frigate-card:menu-add', this._menuAddHandler);
} }
/**
* Disconnected callback.
*/
disconnectedCallback(): void { disconnectedCallback(): void {
this.removeEventListener('frigate-card:menu-add', this._menuAddHandler); this.removeEventListener('frigate-card:menu-add', this._menuAddHandler);
super.disconnectedCallback(); super.disconnectedCallback();
} }
/**
* Render the template.
* @returns A rendered template.
*/
protected render(): TemplateResult { protected render(): TemplateResult {
return html` <frigate-card-elements-core return html` <frigate-card-elements-core
.hass=${this._hass} .hass=${this._hass}
.view=${this.view} .conditionState=${this.conditionState}
.elements=${this.elements} .elements=${this.elements}
> >
</frigate-card-elements-core>`; </frigate-card-elements-core>`;
} }
/**
* Get styles.
*/
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return unsafeCSS(elementsStyle); 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') @customElement('frigate-card-conditional')
export class FrigateCardElementsConditional extends LitElement { export class FrigateCardElementsConditional extends LitElement {
protected _config: FrigateConditional | null = null; protected _config?: FrigateConditional;
protected _hass!: HomeAssistant & ExtendedHomeAssistant; protected _hass?: HomeAssistant & ExtendedHomeAssistant;
@query('frigate-card-elements-core') @query('frigate-card-elements-core')
_core!: FrigateCardElementsCore; _core?: FrigateCardElementsCore;
/**
* Set the Home Assistant object.
*/
set hass(hass: HomeAssistant & ExtendedHomeAssistant) { set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
if (this._core) { if (this._core) {
this._core.hass = hass; this._core.hass = hass;
@@ -214,22 +258,25 @@ export class FrigateCardElementsConditional extends LitElement {
this._hass = hass; this._hass = hass;
} }
/**
* Set the card configuration.
* @param config The card configuration.
*/
public setConfig(config: FrigateConditional): void { public setConfig(config: FrigateConditional): void {
this._config = config; this._config = config;
} }
// Transparent to elements. /**
* Create a root into which to render. This card is "transparent".
* @returns
*/
createRenderRoot(): LitElement { createRenderRoot(): LitElement {
return this; return this;
} }
protected evaluate(stateEvent: StateRequestEvent): boolean { /**
if (stateEvent.view && this._config.conditions.view) { * Connected callback.
return this._config.conditions.view.includes(stateEvent.view.view); */
}
return true;
}
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
@@ -239,27 +286,11 @@ export class FrigateCardElementsConditional extends LitElement {
this.className = ''; this.className = '';
} }
protected render(): TemplateResult | void { /**
const stateEvent = new StateRequestEvent(`frigate-card:state-request`, { * Render the card.
bubbles: true,
composed: true,
});
/* Special note on what's going on here:
*
* Picture elements all are descendents of <frigate-card-elements>, 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); protected render(): TemplateResult | void {
if (this.evaluate(stateEvent)) { if (fetchStateAndEvaluateCondition(this, this._config.conditions)) {
return html` <frigate-card-elements-core return html` <frigate-card-elements-core
.hass=${this._hass} .hass=${this._hass}
.elements=${this._config.elements} .elements=${this._config.elements}
@@ -274,10 +305,17 @@ export class FrigateCardElementsBaseMenuIcon<T> extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected _config: T | null = null; protected _config: T | null = null;
/**
* Set the card config.
* @param config The configuration.
*/
public setConfig(config: T): void { public setConfig(config: T): void {
this._config = config; this._config = config;
} }
/**
* Connected callback.
*/
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
if (this._config) { if (this._config) {
@@ -285,6 +323,9 @@ export class FrigateCardElementsBaseMenuIcon<T> extends LitElement {
} }
} }
/**
* Disconnected callback.
*/
disconnectedCallback(): void { disconnectedCallback(): void {
if (this._config) { if (this._config) {
dispatchFrigateCardEvent<T>(this, 'menu-remove', this._config); dispatchFrigateCardEvent<T>(this, 'menu-remove', this._config);
+5 -1
View File
@@ -26,6 +26,7 @@ import {
} from '../common.js'; } from '../common.js';
import menuStyle from '../scss/menu.scss'; import menuStyle from '../scss/menu.scss';
import { ConditionState, evaluateCondition } from '../card-condition.js';
export const MENU_HEIGHT = 46; export const MENU_HEIGHT = 46;
export const FRIGATE_BUTTON_MENU_ICON = 'frigate'; export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
@@ -53,6 +54,9 @@ export class FrigateCardMenu extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public buttons: MenuButton[] = []; public buttons: MenuButton[] = [];
@property({ attribute: false })
protected conditionState?: ConditionState;
/** /**
* Handle an action on a menu button. * Handle an action on a menu button.
* @param ev The action event. * @param ev The action event.
@@ -196,7 +200,7 @@ export class FrigateCardMenu extends LitElement {
} }
const mode = this._menuConfig.mode; const mode = this._menuConfig.mode;
if (mode == 'none') { if (mode == 'none' || !evaluateCondition(this._menuConfig.conditions, this.conditionState)) {
return; return;
} }
+8 -3
View File
@@ -267,11 +267,15 @@ export const menuStateIconSchema = stateIconSchema.merge(
); );
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>; export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
const frigateCardConditionSchema = z.object({
view: z.string().array().optional(),
fullscreen: z.boolean().optional(),
});
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
const frigateConditionalSchema = z.object({ const frigateConditionalSchema = z.object({
type: z.literal('custom:frigate-card-conditional'), type: z.literal('custom:frigate-card-conditional'),
conditions: z.object({ conditions: frigateCardConditionSchema,
view: z.string().array().optional(),
}),
elements: z.lazy(() => pictureElementsSchema), elements: z.lazy(() => pictureElementsSchema),
}); });
export type FrigateConditional = z.infer<typeof frigateConditionalSchema>; export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
@@ -407,6 +411,7 @@ const menuConfigSchema = z
}) })
.default(menuConfigDefault.buttons), .default(menuConfigDefault.buttons),
button_size: z.string().default(menuConfigDefault.button_size), button_size: z.string().default(menuConfigDefault.button_size),
conditions: frigateCardConditionSchema.optional(),
}) })
.default(menuConfigDefault); .default(menuConfigDefault);
export type MenuConfig = z.infer<typeof menuConfigSchema>; export type MenuConfig = z.infer<typeof menuConfigSchema>;