Initial version of status bar.
This commit is contained in:
@@ -23,7 +23,7 @@ Only the `cameras` option is required, all other parameters are optional.
|
||||
| [`cameras_global`](cameras/README.md) | Global defaults that apply to all cameras from the `cameras` section. |
|
||||
| `card_id` | An optional ID to uniquely identify this card. For use when actions are being sent to card(s) via [URL actions](../usage/url-actions.md). Must exclusively consist of these characters: `[a-zA-Z0-9_]`. |
|
||||
| [`dimensions`](dimensions.md) | Configures the overall card dimensions. |
|
||||
| [`elements`](elements.md) | Add custom elements to the card. |
|
||||
| [`elements`](elements/README.md) | Add custom elements to the card. |
|
||||
| [`image`](image.md) | Configures the `image` view. |
|
||||
| [`live`](live.md) | Configures the `live` view. |
|
||||
| [`media_gallery`](media-gallery.md) | Configures the media gallery. |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- [`cameras`](cameras/README.md)
|
||||
- [`conditions`](conditions.md)
|
||||
- [`dimensions`](dimensions.md)
|
||||
- [`elements`](elements.md)
|
||||
- [`elements`](elements/README.md)
|
||||
- [`image`](image.md)
|
||||
- [`live`](live.md)
|
||||
- [`media_gallery`](media-gallery.md)
|
||||
@@ -14,6 +14,7 @@
|
||||
- [`overrides`](overrides.md)
|
||||
- [`performance`](performance.md)
|
||||
- [`profiles`](profiles.md)
|
||||
- [`status_bar`](status-bar.md)
|
||||
- [`timeline`](timeline.md)
|
||||
- [`view`](view.md)
|
||||
- [Examples](../examples.md)
|
||||
|
||||
@@ -7,7 +7,7 @@ multiple other blocks.
|
||||
|
||||
Actions are pre-configured activities that can be triggered in response to a
|
||||
variety of circumstances (e.g. tapping on a menu icon, double tapping on an
|
||||
[element](../elements.md) or holding the mouse/tap down on a particular
|
||||
[element](../elements/README.md) or holding the mouse/tap down on a particular
|
||||
[view](../view.md?id=supported-views)).
|
||||
|
||||
### Differences in actions between Frigate Card and Home Assistant
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [`cameras`](../cameras/README.md)
|
||||
- [`conditions`](../conditions.md)
|
||||
- [`dimensions`](../dimensions.md)
|
||||
- [`elements`](../elements.md)
|
||||
- [`elements`](../elements/README.md)
|
||||
- [`image`](../image.md)
|
||||
- [`live`](../live.md)
|
||||
- [`media_gallery`](../media-gallery.md)
|
||||
@@ -16,8 +16,10 @@
|
||||
- [`overrides`](../overrides.md)
|
||||
- [`performance`](../performance.md)
|
||||
- [`profiles`](../profiles.md)
|
||||
- [`status_bar`](../status-bar.md)
|
||||
- [`timeline`](../timeline.md)
|
||||
- [`view`](../view.md)
|
||||
- [Examples](../../examples.md)
|
||||
- [Screenshots](../../screenshots.md)
|
||||
- [Troubleshooting](../../troubleshooting.md)
|
||||
- [Usage](../../usage/README.md)
|
||||
|
||||
@@ -450,15 +450,25 @@ action: custom:frigate-card-action
|
||||
frigate_card_action: snapshot
|
||||
```
|
||||
|
||||
## `snapshots`
|
||||
## `status_bar`
|
||||
|
||||
Change to the `snapshots` view.
|
||||
Add or remove items from the status bar.
|
||||
|
||||
```yaml
|
||||
action: custom:frigate-card-action
|
||||
frigate_card_action: snapshots
|
||||
frigate_card_action: status_bar
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `status_bar_action` | | If `add` adds `items` to the status bar, if `remove` removes items and if `reset` resets the status bar entirely (removes all dynamically added items). |
|
||||
| `items` | | The items to `add` or `remove`. See below. |
|
||||
|
||||
### `items`
|
||||
|
||||
The items parameter is a list of items to `add` or `remove`. See [`custom:frigate-card-status-bar-icon`](../../elements/custom/README.md?id=status-bar-icon), [`custom:frigate-card-status-bar-image`](../../elements/custom/README.md?id=status-bar-image), [`custom:frigate-card-status-bar-string`](../../elements/custom/README.md?id=status-bar-string) for the allowable items and their parameters. See the [fully expanded reference](./README.md?fully-expanded-reference) below for a complete example.
|
||||
|
||||
## `timeline`
|
||||
|
||||
Change to the `timeline` view.
|
||||
@@ -732,4 +742,69 @@ elements:
|
||||
tap_action:
|
||||
action: custom:frigate-card-action
|
||||
frigate_card_action: unmute
|
||||
- type: custom:frigate-card-menu-icon
|
||||
icon: mdi:alpha-h-circle-outline
|
||||
title: Add status bar contents
|
||||
tap_action:
|
||||
- action: custom:frigate-card-action
|
||||
frigate_card_action: status_bar
|
||||
status_bar_action: add
|
||||
items:
|
||||
- type: custom:frigate-card-status-bar-string
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
string: 'Intruder alert!'
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-status-bar-icon
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
icon: 'mdi:cow'
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-status-bar-image
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
image: https://my.site.com/status.png
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-menu-icon
|
||||
icon: mdi:alpha-i-circle-outline
|
||||
title: Remove status bar contents
|
||||
tap_action:
|
||||
- action: custom:frigate-card-action
|
||||
frigate_card_action: status_bar
|
||||
status_bar_action: remove
|
||||
items:
|
||||
- type: custom:frigate-card-status-bar-string
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
string: 'Intruder alert!'
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-status-bar-icon
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
icon: 'mdi:cow'
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-status-bar-image
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
image: https://my.site.com/status.png
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-menu-icon
|
||||
icon: mdi:alpha-i-circle-outline
|
||||
title: Reset status bar contents
|
||||
tap_action:
|
||||
- action: custom:frigate-card-action
|
||||
frigate_card_action: status_bar
|
||||
status_bar_action: reset
|
||||
```
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [`image`](../../image.md)
|
||||
- [`live`](../../live.md)
|
||||
- [`media_gallery`](../../media-gallery.md)
|
||||
@@ -16,8 +16,10 @@
|
||||
- [`overrides`](../../overrides.md)
|
||||
- [`performance`](../../performance.md)
|
||||
- [`profiles`](../../profiles.md)
|
||||
- [`status_bar`](../../status-bar.md)
|
||||
- [`timeline`](../../timeline.md)
|
||||
- [`view`](../../view.md)
|
||||
- [Examples](../../../examples.md)
|
||||
- [Screenshots](../../../screenshots.md)
|
||||
- [Troubleshooting](../../../troubleshooting.md)
|
||||
- [Usage](../../../usage/README.md)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [`image`](../../image.md)
|
||||
- [`live`](../../live.md)
|
||||
- [`media_gallery`](../../media-gallery.md)
|
||||
@@ -16,8 +16,10 @@
|
||||
- [`overrides`](../../overrides.md)
|
||||
- [`performance`](../../performance.md)
|
||||
- [`profiles`](../../profiles.md)
|
||||
- [`status_bar`](../../status-bar.md)
|
||||
- [`timeline`](../../timeline.md)
|
||||
- [`view`](../../view.md)
|
||||
- [Examples](../../../examples.md)
|
||||
- [Screenshots](../../../screenshots.md)
|
||||
- [Troubleshooting](../../../troubleshooting.md)
|
||||
- [Usage](../../../usage/README.md)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [`engine`](engine.md)
|
||||
- [`conditions`](../conditions.md)
|
||||
- [`dimensions`](../dimensions.md)
|
||||
- [`elements`](../elements.md)
|
||||
- [`elements`](../elements/README.md)
|
||||
- [`image`](../image.md)
|
||||
- [`live`](../live.md)
|
||||
- [`media_gallery`](../media-gallery.md)
|
||||
@@ -16,8 +16,10 @@
|
||||
- [`overrides`](../overrides.md)
|
||||
- [`performance`](../performance.md)
|
||||
- [`profiles`](../profiles.md)
|
||||
- [`status_bar`](../status-bar.md)
|
||||
- [`timeline`](../timeline.md)
|
||||
- [`view`](../view.md)
|
||||
- [Examples](../../examples.md)
|
||||
- [Screenshots](../../screenshots.md)
|
||||
- [Troubleshooting](../../troubleshooting.md)
|
||||
- [Usage](../../usage/README.md)
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
# `elements`
|
||||
|
||||
This card supports the [Picture Elements configuration
|
||||
syntax](https://www.home-assistant.io/lovelace/picture-elements/) to seamlessly
|
||||
allow the user to add custom elements to the card.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- [element_1]
|
||||
- [element_2]
|
||||
```
|
||||
|
||||
?> The Frigate Card allows either a single [action](actions/README.md) (as in stock Home
|
||||
Assistant) or list of [actions](actions/README.md) to be defined for each class of user interaction
|
||||
(e.g. `tap`, `double_tap`, `hold`, etc). See [an example of multiple actions](../examples.md?id=multiple-actions).
|
||||
|
||||
## `conditional`
|
||||
|
||||
This element will let you show its sub-elements based on entity states. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#conditional-element).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: conditional
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `custom`
|
||||
|
||||
Custom elements provided by a card. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#custom-elements).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `custom:frigate-card-menu-icon`
|
||||
|
||||
Add an arbitrary icon to the Frigate Card menu. Configuration is ~identical to that of the [Picture Elements Icon](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) except with a type name of `custom:frigate-card-menu-icon`.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `custom:frigate-card-menu-state-icon`
|
||||
|
||||
Add a state icon to the Frigate Card menu that represents the state of a Home Assistant entity. Configuration is ~identical to that of the [Picture Elements State Icon](https://www.home-assistant.io/lovelace/picture-elements/#state-icon) except with a type name of `custom:frigate-card-menu-state-icon`.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `custom:frigate-card-menu-submenu`
|
||||
|
||||
Add a configurable submenu dropdown.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-submenu
|
||||
# [...]
|
||||
```
|
||||
|
||||
Parameters for this element are identical to the parameters of the [stock Home Assistant Icon Element](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) with the exception of these parameters which differ:
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ------------------------------------------- |
|
||||
| `type` | Must be `custom:frigate-card-menu-submenu`. |
|
||||
| `items` | A list of menu items, as described below. |
|
||||
|
||||
### Submenu items
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `true` | Whether or not to show this item as enabled / selectable. |
|
||||
| `entity` | | An optional Home Assistant entity from which title, icon and style can be automatically computed. |
|
||||
| `icon` | | An optional item icon to display, e.g. `mdi:car` |
|
||||
| `selected` | `false` | Whether or not to show this item as selected. |
|
||||
| `state_color` | `true` | Whether or not the title and icon should be stylized based on state. |
|
||||
| `style` | | Position and style the element using CSS. |
|
||||
| `tap_action`, `double_tap_action`, `hold_action`, `start_tap`, `end_tap` | | The [actions](actions/README.md) to take when this item is interacted with. |
|
||||
| `title` | | An optional title to display. |
|
||||
|
||||
## `custom:frigate-card-menu-submenu-select`
|
||||
|
||||
Add a submenu based on a `select` or `input_select`. This element allows you to convert a [Home Assistant Select Entity](https://www.home-assistant.io/integrations/select/) or [Home Assistant Input Select Entity](https://www.home-assistant.io/integrations/input_select/) (an entity either starting with `select` or `input_select`) into an overridable submenu. This _could_ be done by hand using a regular submenu (above) -- this element is a convenience.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-submenu-select
|
||||
# [...]
|
||||
```
|
||||
|
||||
Parameters for the `custom:frigate-card-menu-submenu-select` element are identical to the parameters of the [stock Home Assistant State Icon Element](https://www.home-assistant.io/dashboards/picture-elements/#state-icon) with the exception of these parameters which differ:
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Must be `custom:frigate-card-menu-submenu-select`. |
|
||||
| `options` | An optional dictionary of overrides keyed by the option name that the given select entity supports. These options can be used to set or override submenu item parameters on a per-option basis. The format is as described in [Submenu Items](elements.md?id=submenu-items) above. |
|
||||
|
||||
See the `select` [submenu example](../examples.md?id=select-submenu).
|
||||
|
||||
## `custom:frigate-card-conditional`
|
||||
|
||||
Restrict a set of elements to only render when the card is matches a set of [conditions](conditions.md). This is analogous to [`conditional`](elements.md?id=conditional) element above, except supporting a rich set of Frigate Card [conditions](conditions.md).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-conditional
|
||||
# [...]
|
||||
```
|
||||
|
||||
Parameters for the `custom:frigate-card-conditional` element:
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Must be `custom:frigate-card-conditional`. |
|
||||
| `conditions` | A list of [conditions](conditions.md) that must evaluate to true in order for the elements to be rendered. |
|
||||
| `elements` | The elements to render. Can be any supported element. |
|
||||
|
||||
See the [conditional elements example](../examples.md?id=conditional-elements).
|
||||
|
||||
## `icon`
|
||||
|
||||
This element creates a static icon that is not linked to the state of an entity. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#icon-element).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `image`
|
||||
|
||||
This creates an image element that overlays the background image. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#image-element).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: image
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `service-button`
|
||||
|
||||
This entity creates a button (with arbitrary text) that can be used to call a service. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#service-call-button).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: service-button
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `state-badge`
|
||||
|
||||
This element creates a badge representing the state of an entity. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#state-badge).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-badge
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `state-icon`
|
||||
|
||||
This element represents an entity state using an icon. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#state-icon).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `state-label`
|
||||
|
||||
This element represents an entity’s state via text. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#state-label).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-label
|
||||
# [...]
|
||||
```
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
> [Actions](actions/README.md) are omitted for simplicity.
|
||||
|
||||
[](common/expanded-warning.md ':include')
|
||||
|
||||
### Stock Home Assistant elements
|
||||
|
||||
Reference: [Home Assistant Picture Elements](https://www.home-assistant.io/dashboards/picture-elements/)
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-badge
|
||||
entity: sensor.kitchen_dining_multisensor_air_temperature
|
||||
style:
|
||||
left: 100px
|
||||
top: 50px
|
||||
title: 'Temperature'
|
||||
- type: state-icon
|
||||
entity: light.office_main_lights
|
||||
icon: mdi:lamp
|
||||
state_color: true
|
||||
style:
|
||||
left: 100px
|
||||
top: 100px
|
||||
- type: state-label
|
||||
entity: sensor.kitchen_motion_sensor_battery
|
||||
attribute: battery_voltage
|
||||
prefix: Volts
|
||||
title: Battery Voltage
|
||||
style:
|
||||
left: 100px
|
||||
top: 150px
|
||||
- type: state-label
|
||||
entity: sensor.kitchen_motion_sensor_battery
|
||||
attribute: battery_voltage
|
||||
prefix: 'Volts: '
|
||||
title: Battery Voltage
|
||||
style:
|
||||
background-color: black
|
||||
left: 100px
|
||||
top: 200px
|
||||
- type: service-button
|
||||
title: Light on
|
||||
service: homeassistant.turn_on
|
||||
service_data:
|
||||
entity: light.office_main_lights
|
||||
style:
|
||||
left: 100px
|
||||
top: 250px
|
||||
- type: icon
|
||||
icon: mdi:cow
|
||||
title: Moo
|
||||
style:
|
||||
left: 100px
|
||||
top: 300px
|
||||
- type: image
|
||||
entity: light.office_main_lights
|
||||
title: Image
|
||||
state_image:
|
||||
on: 'https://picsum.photos/id/1003/1181/1772'
|
||||
off: 'https://picsum.photos/id/102/4320/3240'
|
||||
state_filter:
|
||||
'on': brightness(110%) saturate(1.2)
|
||||
'off': brightness(50%) hue-rotate(45deg)
|
||||
style:
|
||||
left: 100px
|
||||
top: 350px
|
||||
height: 50px
|
||||
width: 100px
|
||||
- type: conditional
|
||||
conditions:
|
||||
- condition: state
|
||||
entity: light.office_main_lights
|
||||
state: on
|
||||
state_not: off
|
||||
- condition: numeric_state
|
||||
entity: sensor.light_level
|
||||
above: 20
|
||||
below: 100
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
elements:
|
||||
- type: icon
|
||||
icon: mdi:dog
|
||||
title: Woof
|
||||
style:
|
||||
left: 100px
|
||||
top: 400px
|
||||
```
|
||||
|
||||
### Frigate Card elements
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-icon
|
||||
icon: mdi:car
|
||||
title: Vroom
|
||||
- type: custom:frigate-card-menu-state-icon
|
||||
entity: light.office_main_lights
|
||||
title: Office lights
|
||||
icon: mdi:chair-rolling
|
||||
state_color: true
|
||||
- type: custom:frigate-card-menu-submenu
|
||||
icon: mdi:menu
|
||||
items:
|
||||
- title: Lights
|
||||
icon: mdi:lightbulb
|
||||
entity: light.office_main_lights
|
||||
tap_action:
|
||||
action: toggle
|
||||
- title: Google
|
||||
icon: mdi:google
|
||||
enabled: false
|
||||
tap_action:
|
||||
action: url
|
||||
url_path: https://www.google.com
|
||||
- type: custom:frigate-card-menu-submenu-select
|
||||
icon: mdi:lamps
|
||||
entity: input_select.kitchen_scene
|
||||
options:
|
||||
scene.kitchen_cooking_scene:
|
||||
icon: mdi:chef-hat
|
||||
title: Cooking time!
|
||||
scene.kitchen_tv_scene:
|
||||
icon: mdi:television
|
||||
title: TV!
|
||||
# Show a pig icon if a variety of conditions are met.
|
||||
- type: custom:frigate-card-conditional
|
||||
elements:
|
||||
- type: icon
|
||||
icon: mdi:pig
|
||||
title: Oink
|
||||
style:
|
||||
left: 300px
|
||||
top: 100px
|
||||
conditions:
|
||||
- condition: view
|
||||
views:
|
||||
- live
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
- condition: expand
|
||||
expand: true
|
||||
- condition: camera
|
||||
cameras: camera.front_door
|
||||
- condition: media_loaded
|
||||
media_loaded: true
|
||||
- condition: display_mode
|
||||
display_mode: single
|
||||
- condition: triggered
|
||||
triggered:
|
||||
- camera.front_door
|
||||
- condition: interaction
|
||||
interaction: true
|
||||
- condition: microphone
|
||||
muted: true
|
||||
connected: true
|
||||
- condition: state
|
||||
entity: light.office_main_lights
|
||||
state: on
|
||||
state_not: off
|
||||
- condition: numeric_state
|
||||
entity: sensor.light_level
|
||||
above: 20
|
||||
below: 100
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
# `elements`
|
||||
|
||||
## Introduction to elements <!-- {docsify-ignore} -->
|
||||
|
||||
This card supports the [Picture Elements configuration
|
||||
syntax](https://www.home-assistant.io/lovelace/picture-elements/) to seamlessly
|
||||
allow the user to add custom elements to the card.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- [element_1]
|
||||
- [element_2]
|
||||
```
|
||||
|
||||
?> The Frigate Card allows either a single [action](../actions/README.md) (as in stock Home
|
||||
Assistant) or list of [actions](../actions/README.md) to be defined for each class of user interaction
|
||||
(e.g. `tap`, `double_tap`, `hold`, etc). See [an example of multiple actions](../../examples.md?id=multiple-actions).
|
||||
|
||||
## Elements <!-- {docsify-ignore} -->
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------------------- | --------------------------------- |
|
||||
| [Custom Elements](./custom/README.md) | Custom elements. |
|
||||
| [Stock Elements](./stock/README.md) | Standard Home Assistant elements. |
|
||||
@@ -0,0 +1,29 @@
|
||||
- [Getting Started](../../README.md)
|
||||
- [Configuration](../README.md)
|
||||
- [`actions`](../actions/README.md)
|
||||
- [`automations`](../automations.md)
|
||||
- [`cameras`](../cameras/README.md)
|
||||
- [`conditions`](../conditions.md)
|
||||
- [`dimensions`](../dimensions.md)
|
||||
- [`elements`](README.md)
|
||||
- [Custom Elements](./custom/README.md)
|
||||
- [Stock Elements](./stock/README.md)
|
||||
- [`image`](../image.md)
|
||||
- [`live`](../live.md)
|
||||
- [`media_gallery`](../media-gallery.md)
|
||||
- [`media_viewer`](../media-viewer.md)
|
||||
- [`menu`](../menu.md)
|
||||
- [`overrides`](../overrides.md)
|
||||
- [`performance`](../performance.md)
|
||||
- [`profiles`](../profiles.md)
|
||||
- [`status_bar`](../status-bar.md)
|
||||
- [`timeline`](../timeline.md)
|
||||
- [`view`](../view.md)
|
||||
- [Examples](../../examples.md)
|
||||
- [Screenshots](../../screenshots.md)
|
||||
- [Troubleshooting](../../troubleshooting.md)
|
||||
- [Usage](../../usage/README.md)
|
||||
|
||||
---
|
||||
|
||||
- [Developing](../../developing.md)
|
||||
@@ -0,0 +1,258 @@
|
||||
# Custom Elements
|
||||
|
||||
## `conditional`
|
||||
|
||||
Restrict a set of elements to only render when the card is matches a set of [conditions](../../conditions.md). This is analogous to the stock [`conditional`](../stock/README.md?id=conditional) element except supporting a rich set of Frigate Card [conditions](../../conditions.md).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-conditional
|
||||
# [...]
|
||||
```
|
||||
|
||||
Parameters for the `custom:frigate-card-conditional` element:
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Must be `custom:frigate-card-conditional`. |
|
||||
| `conditions` | A list of [conditions](../../conditions.md) that must evaluate to true in order for the elements to be rendered. |
|
||||
| `elements` | The elements to render. Can be any supported element. |
|
||||
|
||||
See the [conditional elements example](../../../examples.md?id=conditional-elements).
|
||||
|
||||
## `menu-icon`
|
||||
|
||||
Add an arbitrary icon to the Frigate Card menu. Configuration is ~identical to that of the [Picture Elements Icon](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) except with a type name of `custom:frigate-card-menu-icon`.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `menu-submenu`
|
||||
|
||||
Add a configurable submenu dropdown.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-submenu
|
||||
# [...]
|
||||
```
|
||||
|
||||
Parameters for this element are identical to the parameters of the [stock Home Assistant Icon Element](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) with the exception of these parameters which differ:
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ------------------------------------------- |
|
||||
| `type` | Must be `custom:frigate-card-menu-submenu`. |
|
||||
| `items` | A list of menu items, as described below. |
|
||||
|
||||
### Submenu items
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `true` | Whether or not to show this item as enabled / selectable. |
|
||||
| `entity` | | An optional Home Assistant entity from which title, icon and style can be automatically computed. |
|
||||
| `icon` | | An optional item icon to display, e.g. `mdi:car` |
|
||||
| `selected` | `false` | Whether or not to show this item as selected. |
|
||||
| `state_color` | `true` | Whether or not the title and icon should be stylized based on state. |
|
||||
| `style` | | Position and style the element using CSS. |
|
||||
| `tap_action`, `double_tap_action`, `hold_action`, `start_tap`, `end_tap` | | The [actions](../../actions/README.md) to take when this item is interacted with. |
|
||||
| `title` | | An optional title to display. |
|
||||
|
||||
## `menu-submenu-select`
|
||||
|
||||
Add a submenu based on a `select` or `input_select`. This element allows you to convert a [Home Assistant Select Entity](https://www.home-assistant.io/integrations/select/) or [Home Assistant Input Select Entity](https://www.home-assistant.io/integrations/input_select/) (an entity either starting with `select` or `input_select`) into an overridable submenu. This _could_ be done by hand using a regular submenu (above) -- this element is a convenience.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-submenu-select
|
||||
# [...]
|
||||
```
|
||||
|
||||
Parameters for the `custom:frigate-card-menu-submenu-select` element are identical to the parameters of the [stock Home Assistant State Icon Element](https://www.home-assistant.io/dashboards/picture-elements/#state-icon) with the exception of these parameters which differ:
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Must be `custom:frigate-card-menu-submenu-select`. |
|
||||
| `options` | An optional dictionary of overrides keyed by the option name that the given select entity supports. These options can be used to set or override submenu item parameters on a per-option basis. The format is as described in [Submenu Items](./README.md?id=submenu-items) above. |
|
||||
|
||||
See the `select` [submenu example](../../../examples.md?id=select-submenu).
|
||||
|
||||
## `state-icon`
|
||||
|
||||
Add a state icon to the Frigate Card menu that represents the state of a Home Assistant entity. Configuration is ~identical to that of the [Picture Elements State Icon](https://www.home-assistant.io/lovelace/picture-elements/#state-icon) except with a type name of `custom:frigate-card-menu-state-icon`.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `status-bar-icon`
|
||||
|
||||
Add an arbitrary icon to the status bar.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-status-bar-icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `type` | | Must be `custom:frigate-card-status-bar-icon`. |
|
||||
| `actions` | | Actions to performs when the status bar item is interacted with. See [actions](../../actions/README.md). |
|
||||
| `enabled` | `true` | `true` to enable this status bar item, `false` to disable. |
|
||||
| `exclusive` | `false` | Whether or not this item should evict non-exclusive items from the status bar. |
|
||||
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
||||
| `icon` | | The icon to show in the status bar, e.g. `mdi:camera-front`. |
|
||||
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
||||
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
||||
|
||||
## `status-bar-image`
|
||||
|
||||
Add an arbitrary image to the status bar.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-status-bar-image
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `type` | | Must be `custom:frigate-card-status-bar-image`. |
|
||||
| `actions` | | Actions to performs when the status bar item is interacted with. See [actions](../../actions/README.md). |
|
||||
| `enabled` | `true` | `true` to enable this status bar item, `false` to disable. |
|
||||
| `exclusive` | `false` | Whether or not this item should evict non-exclusive items from the status bar. |
|
||||
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
||||
| `image` | | The image to show in the status bar, e.g. `https://my.site.com/status.png`. |
|
||||
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
||||
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
||||
|
||||
## `status-bar-string`
|
||||
|
||||
Add an arbitrary string to the status bar.
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-status-bar-string
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `type` | | Must be `custom:frigate-card-status-bar-string`. |
|
||||
| `actions` | | Actions to performs when the status bar item is interacted with. See [actions](../../actions/README.md). |
|
||||
| `enabled` | `true` | `true` to enable this status bar item, `false` to disable. |
|
||||
| `exclusive` | `false` | Whether or not this item should evict non-exclusive items from the status bar. |
|
||||
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
||||
| `string` | | The string to show in the status bar, e.g. `Intruder detected!` |
|
||||
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
||||
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
> [Actions](../../actions/README.md) are omitted for simplicity.
|
||||
|
||||
[](../../common/expanded-warning.md ':include')
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom:frigate-card-menu-icon
|
||||
icon: mdi:car
|
||||
title: Vroom
|
||||
- type: custom:frigate-card-menu-state-icon
|
||||
entity: light.office_main_lights
|
||||
title: Office lights
|
||||
icon: mdi:chair-rolling
|
||||
state_color: true
|
||||
- type: custom:frigate-card-menu-submenu
|
||||
icon: mdi:menu
|
||||
items:
|
||||
- title: Lights
|
||||
icon: mdi:lightbulb
|
||||
entity: light.office_main_lights
|
||||
tap_action:
|
||||
action: toggle
|
||||
- title: Google
|
||||
icon: mdi:google
|
||||
enabled: false
|
||||
tap_action:
|
||||
action: url
|
||||
url_path: https://www.google.com
|
||||
- type: custom:frigate-card-menu-submenu-select
|
||||
icon: mdi:lamps
|
||||
entity: input_select.kitchen_scene
|
||||
options:
|
||||
scene.kitchen_cooking_scene:
|
||||
icon: mdi:chef-hat
|
||||
title: Cooking time!
|
||||
scene.kitchen_tv_scene:
|
||||
icon: mdi:television
|
||||
title: TV!
|
||||
# Show a pig icon if a variety of conditions are met.
|
||||
- type: custom:frigate-card-conditional
|
||||
elements:
|
||||
- type: icon
|
||||
icon: mdi:pig
|
||||
title: Oink
|
||||
style:
|
||||
left: 300px
|
||||
top: 100px
|
||||
conditions:
|
||||
- condition: view
|
||||
views:
|
||||
- live
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
- condition: expand
|
||||
expand: true
|
||||
- condition: camera
|
||||
cameras: camera.front_door
|
||||
- condition: media_loaded
|
||||
media_loaded: true
|
||||
- condition: display_mode
|
||||
display_mode: single
|
||||
- condition: triggered
|
||||
triggered:
|
||||
- camera.front_door
|
||||
- condition: interaction
|
||||
interaction: true
|
||||
- condition: microphone
|
||||
muted: true
|
||||
connected: true
|
||||
- condition: state
|
||||
entity: light.office_main_lights
|
||||
state: on
|
||||
state_not: off
|
||||
- condition: numeric_state
|
||||
entity: sensor.light_level
|
||||
above: 20
|
||||
below: 100
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
- type: custom:frigate-card-status-bar-string
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
string: 'Intruder alert!'
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-status-bar-icon
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
icon: 'mdi:cow'
|
||||
priority: 50
|
||||
sufficient: false
|
||||
- type: custom:frigate-card-status-bar-image
|
||||
enabled: true
|
||||
exclusive: false
|
||||
expand: false
|
||||
image: https://my.site.com/status.png
|
||||
priority: 50
|
||||
sufficient: false
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
- [Getting Started](../../../README.md)
|
||||
- [Configuration](../../README.md)
|
||||
- [`actions`](../README.md)
|
||||
- [`automations`](../../automations.md)
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [Custom Actions](README.md)
|
||||
- [Stock Actions](../stock/README.md)
|
||||
- [`image`](../../image.md)
|
||||
- [`live`](../../live.md)
|
||||
- [`media_gallery`](../../media-gallery.md)
|
||||
- [`media_viewer`](../../media-viewer.md)
|
||||
- [`menu`](../../menu.md)
|
||||
- [`overrides`](../../overrides.md)
|
||||
- [`performance`](../../performance.md)
|
||||
- [`profiles`](../../profiles.md)
|
||||
- [`status_bar`](../../status-bar.md)
|
||||
- [`timeline`](../../timeline.md)
|
||||
- [`view`](../../view.md)
|
||||
- [Examples](../../../examples.md)
|
||||
- [Screenshots](../../../screenshots.md)
|
||||
- [Troubleshooting](../../../troubleshooting.md)
|
||||
- [Usage](../../../usage/README.md)
|
||||
|
||||
---
|
||||
|
||||
- [Developing](../../../developing.md)
|
||||
@@ -0,0 +1,173 @@
|
||||
# Stock Elements
|
||||
|
||||
## `conditional`
|
||||
|
||||
This element will let you show its sub-elements based on entity states. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#conditional-element).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: conditional
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `custom`
|
||||
|
||||
Custom elements provided by a card. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#custom-elements).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: custom
|
||||
# [...]
|
||||
```
|
||||
|
||||
?> See [Frigate Card custom elements](../custom//README.md) for the custom elements offered by _this_ card.
|
||||
|
||||
## `icon`
|
||||
|
||||
This element creates a static icon that is not linked to the state of an entity. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#icon-element).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `image`
|
||||
|
||||
This creates an image element that overlays the background image. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#image-element).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: image
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `service-button`
|
||||
|
||||
This entity creates a button (with arbitrary text) that can be used to call a service. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#service-call-button).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: service-button
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `state-badge`
|
||||
|
||||
This element creates a badge representing the state of an entity. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#state-badge).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-badge
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `state-icon`
|
||||
|
||||
This element represents an entity state using an icon. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#state-icon).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-icon
|
||||
# [...]
|
||||
```
|
||||
|
||||
## `state-label`
|
||||
|
||||
This element represents an entity’s state via text. See [Home Assistant elements documentation](https://www.home-assistant.io/dashboards/picture-elements/#state-label).
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-label
|
||||
# [...]
|
||||
```
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
> [Actions](../../actions/README.md) are omitted for simplicity.
|
||||
|
||||
[](../../common/expanded-warning.md ':include')
|
||||
|
||||
Reference: [Home Assistant Picture Elements](https://www.home-assistant.io/dashboards/picture-elements/)
|
||||
|
||||
```yaml
|
||||
elements:
|
||||
- type: state-badge
|
||||
entity: sensor.kitchen_dining_multisensor_air_temperature
|
||||
style:
|
||||
left: 100px
|
||||
top: 50px
|
||||
title: 'Temperature'
|
||||
- type: state-icon
|
||||
entity: light.office_main_lights
|
||||
icon: mdi:lamp
|
||||
state_color: true
|
||||
style:
|
||||
left: 100px
|
||||
top: 100px
|
||||
- type: state-label
|
||||
entity: sensor.kitchen_motion_sensor_battery
|
||||
attribute: battery_voltage
|
||||
prefix: Volts
|
||||
title: Battery Voltage
|
||||
style:
|
||||
left: 100px
|
||||
top: 150px
|
||||
- type: state-label
|
||||
entity: sensor.kitchen_motion_sensor_battery
|
||||
attribute: battery_voltage
|
||||
prefix: 'Volts: '
|
||||
title: Battery Voltage
|
||||
style:
|
||||
background-color: black
|
||||
left: 100px
|
||||
top: 200px
|
||||
- type: service-button
|
||||
title: Light on
|
||||
service: homeassistant.turn_on
|
||||
service_data:
|
||||
entity: light.office_main_lights
|
||||
style:
|
||||
left: 100px
|
||||
top: 250px
|
||||
- type: icon
|
||||
icon: mdi:cow
|
||||
title: Moo
|
||||
style:
|
||||
left: 100px
|
||||
top: 300px
|
||||
- type: image
|
||||
entity: light.office_main_lights
|
||||
title: Image
|
||||
state_image:
|
||||
on: 'https://picsum.photos/id/1003/1181/1772'
|
||||
off: 'https://picsum.photos/id/102/4320/3240'
|
||||
state_filter:
|
||||
'on': brightness(110%) saturate(1.2)
|
||||
'off': brightness(50%) hue-rotate(45deg)
|
||||
style:
|
||||
left: 100px
|
||||
top: 350px
|
||||
height: 50px
|
||||
width: 100px
|
||||
- type: conditional
|
||||
conditions:
|
||||
- condition: state
|
||||
entity: light.office_main_lights
|
||||
state: on
|
||||
state_not: off
|
||||
- condition: numeric_state
|
||||
entity: sensor.light_level
|
||||
above: 20
|
||||
below: 100
|
||||
- condition: user
|
||||
users:
|
||||
- 581fca7fdc014b8b894519cc531f9a04
|
||||
elements:
|
||||
- type: icon
|
||||
icon: mdi:dog
|
||||
title: Woof
|
||||
style:
|
||||
left: 100px
|
||||
top: 400px
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
- [Getting Started](../../../README.md)
|
||||
- [Configuration](../../README.md)
|
||||
- [`actions`](../README.md)
|
||||
- [`automations`](../../automations.md)
|
||||
- [`cameras`](../../cameras/README.md)
|
||||
- [`conditions`](../../conditions.md)
|
||||
- [`dimensions`](../../dimensions.md)
|
||||
- [`elements`](../../elements/README.md)
|
||||
- [Custom Actions](../custom/README.md)
|
||||
- [Stock Actions](README.md)
|
||||
- [`image`](../../image.md)
|
||||
- [`live`](../../live.md)
|
||||
- [`media_gallery`](../../media-gallery.md)
|
||||
- [`media_viewer`](../../media-viewer.md)
|
||||
- [`menu`](../../menu.md)
|
||||
- [`overrides`](../../overrides.md)
|
||||
- [`performance`](../../performance.md)
|
||||
- [`profiles`](../../profiles.md)
|
||||
- [`status_bar`](../../status-bar.md)
|
||||
- [`timeline`](../../timeline.md)
|
||||
- [`view`](../../view.md)
|
||||
- [Examples](../../../examples.md)
|
||||
- [Screenshots](../../../screenshots.md)
|
||||
- [Troubleshooting](../../../troubleshooting.md)
|
||||
- [Usage](../../../usage/README.md)
|
||||
|
||||
---
|
||||
|
||||
- [Developing](../../../developing.md)
|
||||
@@ -41,7 +41,6 @@ live:
|
||||
| `next_previous` | | Configures how the "Next & Previous" controls are shown on the `live` view. See below. |
|
||||
| `thumbnails` | | Configures how thumbnails are shown on the `live` view. See below. |
|
||||
| `timeline` | | Configures how the mini-timeline is shown on the `live` view. See below. |
|
||||
| `title` | | Configures how the camera title is shown on the `live` view. See below. |
|
||||
|
||||
### `next_previous`
|
||||
|
||||
@@ -127,22 +126,6 @@ live:
|
||||
|
||||
[](common/timeline-seek-info.md ':include')
|
||||
|
||||
### `title`
|
||||
|
||||
Configures how the camera title is shown on the live view.
|
||||
|
||||
```yaml
|
||||
live:
|
||||
controls:
|
||||
title:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `duration_seconds` | `2` | The number of seconds to display the title popup. `0` implies forever. |
|
||||
| `mode` | `popup-bottom-right` | How to display the live camera title. Acceptable values: `none`, `popup-top-left`, `popup-top-right`, `popup-bottom-left`, `popup-bottom-right` . |
|
||||
|
||||
## `display`
|
||||
|
||||
Controls whether to show a single or grid `live` view.
|
||||
@@ -230,9 +213,6 @@ live:
|
||||
events_media_type: all
|
||||
show_recordings: true
|
||||
window_seconds: 3600
|
||||
title:
|
||||
mode: popup-bottom-right
|
||||
duration_seconds: 2
|
||||
microphone:
|
||||
always_connected: false
|
||||
disconnect_seconds: 90
|
||||
|
||||
@@ -37,7 +37,6 @@ media_viewer:
|
||||
| `next_previous` | | Configures how the "Next & Previous" controls are shown on the media viewer. See below. |
|
||||
| `thumbnails` | | Configures how thumbnails are shown on the media viewer. See below. |
|
||||
| `timeline` | | Configures how the mini-timeline is shown on the media viewer. See below. |
|
||||
| `title` | | Configures how the media title is shown on the media viewer. See below. |
|
||||
|
||||
### `next_previous`
|
||||
|
||||
@@ -121,22 +120,6 @@ media_viewer:
|
||||
|
||||
[](common/timeline-seek-info.md ':include')
|
||||
|
||||
### `title`
|
||||
|
||||
Configures how the media title is shown on the media viewer.
|
||||
|
||||
```yaml
|
||||
media_viewer:
|
||||
controls:
|
||||
title:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `duration_seconds` | `2` | The number of seconds to display the title popup. `0` implies forever. |
|
||||
| `mode` | `popup-bottom-right` | How to display the Media viewer media title. Acceptable values: `none`, `popup-top-left`, `popup-top-right`, `popup-bottom-left`, `popup-bottom-right` . |
|
||||
|
||||
## `display`
|
||||
|
||||
Controls whether to show a single media item or grid in the media viewer.
|
||||
@@ -205,9 +188,6 @@ media_viewer:
|
||||
events_media_type: all
|
||||
show_recordings: true
|
||||
window_seconds: 3600
|
||||
title:
|
||||
mode: popup-bottom-right
|
||||
duration_seconds: 2
|
||||
display:
|
||||
mode: single
|
||||
grid_selected_width_factor: 2
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# `status_bar`
|
||||
|
||||
Configures the card status bar.
|
||||
|
||||
```yaml
|
||||
status_bar:
|
||||
# [...]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `position` | `bottom` | Whether to place the status bar at the `top` or `bottom` of the card. |
|
||||
| `popup_seconds` | `3` | The number of seconds to display the status bar when using the `popup` style. |
|
||||
| `height` | `46` | The height of the status bar in pixels. |
|
||||
| `items` | | Whether to show or hide built-in status bar items. See below. |
|
||||
| `style` | `popup` | The status bar style to show by default, one of `none`, `hover`, `hover-card`, `overlay`, `outside` or `popup`. See below. |
|
||||
|
||||
## `items`
|
||||
|
||||
All configuration is under:
|
||||
|
||||
```yaml
|
||||
status_bar:
|
||||
items:
|
||||
[item]:
|
||||
# [...]
|
||||
```
|
||||
|
||||
### Available Items
|
||||
|
||||
| Button name | Description |
|
||||
| ------------ | ------------------------------------------------------ |
|
||||
| `engine` | The icon of the camera engine for the relevant camera. |
|
||||
| `resolution` | The detected media resolution (if any). |
|
||||
| `technology` | The detected media technology (if any). |
|
||||
| `title` | The media title. |
|
||||
|
||||
### Options for each item
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `enabled` | `true` | Whether or not to show the item. |
|
||||
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
||||
|
||||
## `style`
|
||||
|
||||
This card supports several menu styles.
|
||||
|
||||
| Key | Description |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `hover-card` | Overlay the status bar over the card contents when the mouse is over the **card**, otherwise it is not shown. |
|
||||
| `hover` | Overlay the status bar over the card contents when the mouse is over the **status bar**, otherwise it is not shown. |
|
||||
| `none` | No status bar is shown. |
|
||||
| `outside` | Render the status bar outside the card (i.e. above it if `position` is `top`, or below it if `position` is `bottom`). |
|
||||
| `overlay` | Overlay the status bar over the card contents. |
|
||||
|
||||
## Fully expanded reference
|
||||
|
||||
?> To add custom status bar contents, see [status bar custom elements](elements/custom/README.md?id=status-bar-icon).
|
||||
|
||||
[](common/expanded-warning.md ':include')
|
||||
|
||||
```yaml
|
||||
status_bar:
|
||||
position: bottom
|
||||
popup_seconds: 3
|
||||
height: 46
|
||||
style: popup
|
||||
items:
|
||||
engine:
|
||||
enabled: true
|
||||
priority: 50
|
||||
resolution:
|
||||
enabled: true
|
||||
priority: 50
|
||||
technology:
|
||||
enabled: true
|
||||
priority: 50
|
||||
title:
|
||||
enabled: true
|
||||
priority: 50
|
||||
```
|
||||
@@ -695,6 +695,52 @@ image:
|
||||
refresh_seconds: 10
|
||||
```
|
||||
|
||||
## Status bar
|
||||
|
||||
### Disable status bar
|
||||
|
||||
This example disables the status bar.
|
||||
|
||||
```yaml
|
||||
type: custom:frigate-card
|
||||
cameras:
|
||||
- camera_entity: camera.office
|
||||
status_bar:
|
||||
style: none
|
||||
```
|
||||
|
||||
### Dynamic status bar contents
|
||||
|
||||
This example shows an icon and a message on the status bar when a camera is triggered, replacing the existing contents of the status bar through the use of `exclusive`.
|
||||
|
||||
```yaml
|
||||
type: custom:frigate-card
|
||||
cameras:
|
||||
- camera_entity: camera.office
|
||||
automations:
|
||||
- conditions:
|
||||
- condition: triggered
|
||||
triggered:
|
||||
- camera.office
|
||||
actions:
|
||||
- action: custom:frigate-card-action
|
||||
frigate_card_action: status_bar
|
||||
status_bar_action: add
|
||||
items:
|
||||
- type: custom:frigate-card-status-bar-icon
|
||||
icon: mdi:alarm-light
|
||||
exclusive: true
|
||||
- type: custom:frigate-card-status-bar-string
|
||||
string: Intruder detected!
|
||||
expand: true
|
||||
exclusive: true
|
||||
sufficient: true
|
||||
actions_not:
|
||||
- action: custom:frigate-card-action
|
||||
frigate_card_action: status_bar
|
||||
status_bar_action: reset
|
||||
```
|
||||
|
||||
## Submenus
|
||||
|
||||
You can add submenus to the menu -- buttons that when pressed reveal a dropdown submenu of configurable options.
|
||||
|
||||
+6
-13
@@ -78,8 +78,8 @@ The Android video player swallows `double_tap` interactions in order to
|
||||
rewind or fast-forward. Workarounds:
|
||||
|
||||
- Use `hold` instead of `double_tap` for your card-wide action.
|
||||
- Use a [Frigate Card Element](configuration/elements.md) or menu icon to trigger
|
||||
the action instead.
|
||||
- Use a [Frigate Card Element](configuration/elements/README.md) or menu icon to
|
||||
trigger the action instead.
|
||||
|
||||
### Dragging in carousels broken in Firefox
|
||||
|
||||
@@ -180,20 +180,13 @@ image to specifically allow `crossorigin` requests (which is typically not the
|
||||
case for an image served from a camera, for example). The stock Home Assistant
|
||||
Picture Glance card has the same limitation, for the same reasons.
|
||||
|
||||
### Title "Popups" continually popping up
|
||||
### Status "popup" continually popping up
|
||||
|
||||
Title popups can be disabled for live or media viewer views with this
|
||||
configuration:
|
||||
Status popup can be disabled with this configuration:
|
||||
|
||||
```yaml
|
||||
live:
|
||||
controls:
|
||||
title:
|
||||
mode: none
|
||||
media_viewer:
|
||||
controls:
|
||||
title:
|
||||
mode: none
|
||||
status_bar:
|
||||
style: none
|
||||
```
|
||||
|
||||
### Watermark shown on livestream
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { StatusBarActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class StatusBarAction extends FrigateCardAction<StatusBarActionConfig> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
switch (this._action.status_bar_action) {
|
||||
case 'reset':
|
||||
api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
|
||||
break;
|
||||
case 'add':
|
||||
this._action.items?.forEach((item) =>
|
||||
api.getStatusBarItemManager().addDynamicStatusBarItem(item),
|
||||
);
|
||||
break;
|
||||
case 'remove':
|
||||
this._action.items?.forEach((item) =>
|
||||
api.getStatusBarItemManager().removeDynamicStatusBarItem(item),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,12 @@ import { MuteAction } from './actions/mute';
|
||||
import { PauseAction } from './actions/pause';
|
||||
import { PlayAction } from './actions/play';
|
||||
import { PTZAction } from './actions/ptz';
|
||||
import { PTZControlsAction } from './actions/ptz-controls';
|
||||
import { PTZDigitalAction } from './actions/ptz-digital';
|
||||
import { PTZMultiAction } from './actions/ptz-multi';
|
||||
import { ScreenshotAction } from './actions/screenshot';
|
||||
import { PTZControlsAction } from './actions/ptz-controls';
|
||||
import { SleepAction } from './actions/sleep';
|
||||
import { StatusBarAction } from './actions/status-bar';
|
||||
import { SubstreamOffAction } from './actions/substream-off';
|
||||
import { SubstreamOnAction } from './actions/substream-on';
|
||||
import { SubstreamSelectAction } from './actions/substream-select';
|
||||
@@ -121,6 +122,8 @@ export class ActionFactory {
|
||||
return new PTZControlsAction(context, frigateCardAction, options?.config);
|
||||
case 'log':
|
||||
return new LogAction(context, frigateCardAction, options?.config);
|
||||
case 'status_bar':
|
||||
return new StatusBarAction(context, frigateCardAction, options?.config);
|
||||
}
|
||||
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
|
||||
@@ -96,6 +96,8 @@ export class ConfigManager {
|
||||
this._api.getMessageManager().reset();
|
||||
this._api.getStyleManager().setPerformance();
|
||||
this._api.getCardElementManager().update();
|
||||
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
|
||||
|
||||
setKeyboardShortcutsFromConfig(this._api, this);
|
||||
setAutomationsFromConfig(this._api);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { MediaPlayerManager } from './media-player-manager';
|
||||
import { MessageManager } from './message-manager';
|
||||
import { MicrophoneManager } from './microphone-manager';
|
||||
import { QueryStringManager } from './query-string-manager';
|
||||
import { StatusBarItemManager } from './status-bar-item-manager';
|
||||
import { StyleManager } from './style-manager';
|
||||
import { TriggersManager } from './triggers-manager';
|
||||
import {
|
||||
@@ -110,6 +111,7 @@ export class CardController
|
||||
protected _messageManager = new MessageManager(this);
|
||||
protected _microphoneManager = new MicrophoneManager(this);
|
||||
protected _queryStringManager = new QueryStringManager(this);
|
||||
protected _statusBarItemManager = new StatusBarItemManager(this);
|
||||
protected _styleManager = new StyleManager(this);
|
||||
protected _triggersManager = new TriggersManager(this);
|
||||
protected _viewManager = new ViewManager(this);
|
||||
@@ -228,6 +230,10 @@ export class CardController
|
||||
return this._resolvedMediaCache;
|
||||
}
|
||||
|
||||
public getStatusBarItemManager(): StatusBarItemManager {
|
||||
return this._statusBarItemManager;
|
||||
}
|
||||
|
||||
public static getStubConfig(entities: string[]): FrigateCardConfig {
|
||||
const cameraEntity = entities.find((element) => element.startsWith('camera.'));
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { StatusBarConfig, StatusBarItem } from '../config/types';
|
||||
import { MediaLoadedInfo } from '../types';
|
||||
import { View } from '../view/view';
|
||||
import { CardStatusBarAPI } from './types';
|
||||
|
||||
const RESOLUTION_TOLERANCE_PCT = 0.01;
|
||||
|
||||
export class StatusBarItemManager {
|
||||
protected _api: CardStatusBarAPI;
|
||||
|
||||
constructor(api: CardStatusBarAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
protected _items: StatusBarItem[] = [];
|
||||
protected _dynamicItems: StatusBarItem[] = [];
|
||||
|
||||
public addDynamicStatusBarItem(item: StatusBarItem): void {
|
||||
if (!this._dynamicItems.includes(item)) {
|
||||
this._dynamicItems.push(item);
|
||||
}
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
public removeDynamicStatusBarItem(item: StatusBarItem): void {
|
||||
this._dynamicItems = this._dynamicItems.filter(
|
||||
(existingItem) => !isEqual(existingItem, item),
|
||||
);
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
public removeAllDynamicStatusBarItems(): void {
|
||||
this._dynamicItems = [];
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
public calculateItems(options?: {
|
||||
statusConfig?: StatusBarConfig | null;
|
||||
cameraManager?: CameraManager | null;
|
||||
view?: View | null;
|
||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
}): StatusBarItem[] {
|
||||
const cameraMetadata = options?.view
|
||||
? options?.cameraManager?.getCameraMetadata(options?.view?.camera)
|
||||
: null;
|
||||
const engineLogoIcon = cameraMetadata?.engineLogo ?? null;
|
||||
const title = options?.view?.is('live')
|
||||
? cameraMetadata?.title ?? null
|
||||
: options?.view?.isAnyMediaView()
|
||||
? options?.view.queryResults?.getSelectedResult()?.getTitle() ?? null
|
||||
: null;
|
||||
const resolution = options?.mediaLoadedInfo
|
||||
? this._calculateResolution(options?.mediaLoadedInfo)
|
||||
: null;
|
||||
const technology = options?.mediaLoadedInfo?.technology?.length
|
||||
? options?.mediaLoadedInfo.technology[0]
|
||||
: null;
|
||||
|
||||
return [
|
||||
...(title
|
||||
? [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: title,
|
||||
expand: true,
|
||||
sufficient: true,
|
||||
...options?.statusConfig?.items.title,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(resolution
|
||||
? [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: resolution,
|
||||
...options?.statusConfig?.items.resolution,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(technology && technology === 'webrtc'
|
||||
? [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-icon' as const,
|
||||
icon: 'mdi:webrtc',
|
||||
...options?.statusConfig?.items.technology,
|
||||
},
|
||||
]
|
||||
: !!technology
|
||||
? [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: technology.toUpperCase(),
|
||||
...options?.statusConfig?.items.technology,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(engineLogoIcon
|
||||
? [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-image' as const,
|
||||
image: engineLogoIcon,
|
||||
...options?.statusConfig?.items.engine,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...this._dynamicItems,
|
||||
];
|
||||
}
|
||||
|
||||
protected _matchesWidthHeight(
|
||||
mediaLoadedInfo: MediaLoadedInfo | null,
|
||||
width: number,
|
||||
height: number,
|
||||
): boolean {
|
||||
const widthMin = width * (1 - RESOLUTION_TOLERANCE_PCT);
|
||||
const widthMax = width * (1 + RESOLUTION_TOLERANCE_PCT);
|
||||
const heightMin = height * (1 - RESOLUTION_TOLERANCE_PCT);
|
||||
const heightMax = height * (1 + RESOLUTION_TOLERANCE_PCT);
|
||||
|
||||
const matchesDimension = (val: number, min: number, max: number): boolean => {
|
||||
return val >= min && val <= max;
|
||||
};
|
||||
|
||||
// Allows matching the resolution width and height in either orientation,
|
||||
// and within RESOLUTION_TOLERANCE_PCT of the resolution.
|
||||
return (
|
||||
!!mediaLoadedInfo &&
|
||||
((matchesDimension(mediaLoadedInfo.width, widthMin, widthMax) &&
|
||||
matchesDimension(mediaLoadedInfo.height, heightMin, heightMax)) ||
|
||||
(matchesDimension(mediaLoadedInfo.height, widthMin, widthMax) &&
|
||||
matchesDimension(mediaLoadedInfo.width, heightMin, heightMax)))
|
||||
);
|
||||
}
|
||||
|
||||
protected _calculateResolution(mediaLoadedInfo: MediaLoadedInfo): string {
|
||||
// Ordered roughly by a guess at most common towards the top.
|
||||
if (this._matchesWidthHeight(mediaLoadedInfo, 1920, 1080)) {
|
||||
return '1080p';
|
||||
} else if (this._matchesWidthHeight(mediaLoadedInfo, 1280, 720)) {
|
||||
return '720p';
|
||||
} else if (this._matchesWidthHeight(mediaLoadedInfo, 640, 480)) {
|
||||
return 'VGA';
|
||||
} else if (this._matchesWidthHeight(mediaLoadedInfo, 3840, 2160)) {
|
||||
return '4K';
|
||||
} else if (this._matchesWidthHeight(mediaLoadedInfo, 720, 480)) {
|
||||
return '480p';
|
||||
} else if (this._matchesWidthHeight(mediaLoadedInfo, 720, 576)) {
|
||||
return '576p';
|
||||
} else if (this._matchesWidthHeight(mediaLoadedInfo, 7680, 4320)) {
|
||||
return '8K';
|
||||
} else {
|
||||
return `${mediaLoadedInfo.width}x${mediaLoadedInfo.height}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,30 @@
|
||||
import type { CameraManager } from '../camera-manager/manager';
|
||||
import type { ConditionsManager } from './conditions-manager';
|
||||
import type { Automation } from '../config/types';
|
||||
import type { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import type { ActionsManager } from './actions/actions-manager';
|
||||
import type { DefaultManager } from './default-manager';
|
||||
import type { AutomationsManager } from './automations-manager';
|
||||
import type { CameraURLManager } from './camera-url-manager';
|
||||
import type { CardElementManager } from './card-element-manager';
|
||||
import type { ConditionsManager } from './conditions-manager';
|
||||
import type { ConfigManager } from './config/config-manager';
|
||||
import type { DefaultManager } from './default-manager';
|
||||
import type { DownloadManager } from './download-manager';
|
||||
import type { ExpandManager } from './expand-manager';
|
||||
import type { FullscreenManager } from './fullscreen-manager';
|
||||
import type { HASSManager } from './hass-manager';
|
||||
import type { InitializationManager } from './initialization-manager';
|
||||
import type { InteractionManager } from './interaction-manager';
|
||||
import type { KeyboardStateManager } from './keyboard-state-manager';
|
||||
import type { MediaLoadedInfoManager } from './media-info-manager';
|
||||
import type { MediaPlayerManager } from './media-player-manager';
|
||||
import type { MessageManager } from './message-manager';
|
||||
import type { MicrophoneManager } from './microphone-manager';
|
||||
import type { QueryStringManager } from './query-string-manager';
|
||||
import type { StatusBarItemManager } from './status-bar-item-manager';
|
||||
import type { StyleManager } from './style-manager';
|
||||
import type { TriggersManager } from './triggers-manager';
|
||||
import type { ViewManager } from './view/view-manager';
|
||||
import type { QueryStringManager } from './query-string-manager';
|
||||
import { KeyboardStateManager } from './keyboard-state-manager';
|
||||
import { Automation } from '../config/types';
|
||||
|
||||
// *************************************************************************
|
||||
// Manager APIs
|
||||
@@ -47,6 +48,7 @@ export interface CardActionsAPI {
|
||||
getMediaPlayerManager(): MediaPlayerManager;
|
||||
getMessageManager(): MessageManager;
|
||||
getMicrophoneManager(): MicrophoneManager;
|
||||
getStatusBarItemManager(): StatusBarItemManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
@@ -89,6 +91,7 @@ export interface CardConfigAPI {
|
||||
getInitializationManager(): InitializationManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
getMessageManager(): MessageManager;
|
||||
getStatusBarItemManager(): StatusBarItemManager;
|
||||
getStyleManager(): StyleManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
@@ -218,6 +221,10 @@ export interface CardQueryStringAPI {
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
export interface CardStatusBarAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
}
|
||||
|
||||
export interface CardStyleAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
|
||||
+104
-8
@@ -2,6 +2,7 @@ import { HomeAssistant, LovelaceCardEditor } from '@dermotduffy/custom-card-help
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import 'web-dialog';
|
||||
@@ -16,10 +17,18 @@ import './components/menu.js';
|
||||
import { FrigateCardMenu } from './components/menu.js';
|
||||
import './components/message.js';
|
||||
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
||||
import './components/overlay.js';
|
||||
import { FrigateCardOverlay } from './components/overlay.js';
|
||||
import './components/status-bar';
|
||||
import './components/thumbnail-carousel.js';
|
||||
import './components/views.js';
|
||||
import { FrigateCardViews } from './components/views.js';
|
||||
import { FrigateCardConfig, MenuItem, RawFrigateCardConfig } from './config/types';
|
||||
import {
|
||||
FrigateCardConfig,
|
||||
MenuItem,
|
||||
RawFrigateCardConfig,
|
||||
StatusBarItem,
|
||||
} from './config/types';
|
||||
import { REPO_URL } from './const.js';
|
||||
import { localize } from './localize/localize.js';
|
||||
import cardStyle from './scss/card.scss';
|
||||
@@ -103,6 +112,7 @@ class FrigateCard extends LitElement {
|
||||
protected _menuButtonController = new MenuButtonController();
|
||||
|
||||
protected _refMenu: Ref<FrigateCardMenu> = createRef();
|
||||
protected _refOverlay: Ref<FrigateCardOverlay> = createRef();
|
||||
protected _refMain: Ref<HTMLElement> = createRef();
|
||||
protected _refElements: Ref<FrigateCardElements> = createRef();
|
||||
protected _refViews: Ref<FrigateCardViews> = createRef();
|
||||
@@ -185,7 +195,64 @@ class FrigateCard extends LitElement {
|
||||
this._controller.getInitializationManager().initializeBackgroundIfNecessary();
|
||||
}
|
||||
|
||||
protected _renderMenu(): TemplateResult | void {
|
||||
protected _renderMenuStatusContainer(
|
||||
position: 'top' | 'bottom' | 'overlay',
|
||||
): TemplateResult | void {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuStyle = this._config.menu.style;
|
||||
const menuPosition = this._config.menu.position;
|
||||
const statusBarStyle = this._config.status_bar.style;
|
||||
const statusBarPosition = this._config.status_bar.position;
|
||||
|
||||
if (
|
||||
// If there's nothing to render...
|
||||
(menuStyle === 'none' && statusBarStyle === 'none') ||
|
||||
// ... or the position I'm rendering does not contain the menu/status bar
|
||||
(position !== 'overlay' &&
|
||||
menuPosition !== position &&
|
||||
statusBarPosition !== position)
|
||||
) {
|
||||
// ... then there's nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
const getContents = (kind: 'overlay' | 'outerlay'): TemplateResult => {
|
||||
const shouldRenderMenu =
|
||||
menuStyle !== 'none' &&
|
||||
((menuStyle === 'outside' && kind === 'outerlay') ||
|
||||
(menuStyle !== 'outside' && kind === 'overlay'));
|
||||
|
||||
const shouldRenderStatusBar =
|
||||
statusBarStyle !== 'none' &&
|
||||
((statusBarStyle === 'outside' && kind === 'outerlay') ||
|
||||
(statusBarStyle !== 'outside' && kind === 'overlay'));
|
||||
|
||||
// As an exception, to improve the visual flow, if the menu is being
|
||||
// rendered on the bottom, render the status bar first.
|
||||
return html`
|
||||
${shouldRenderMenu && menuPosition !== 'bottom'
|
||||
? this._renderMenu(menuPosition)
|
||||
: ''}
|
||||
${shouldRenderStatusBar ? this._renderStatusBar(statusBarPosition) : ''}
|
||||
${shouldRenderMenu && menuPosition === 'bottom'
|
||||
? this._renderMenu(menuPosition)
|
||||
: ''}
|
||||
`;
|
||||
};
|
||||
|
||||
return html`
|
||||
${position === 'overlay'
|
||||
? html`<frigate-card-overlay>${getContents('overlay')}</frigate-card-overlay>`
|
||||
: html`<div class="outerlay" data-position="${position}">
|
||||
${getContents('outerlay')}
|
||||
</div>`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderMenu(slot?: string): TemplateResult | void {
|
||||
const view = this._controller.getViewManager().getView();
|
||||
if (!this._hass || !this._config || !view) {
|
||||
return;
|
||||
@@ -193,6 +260,7 @@ class FrigateCard extends LitElement {
|
||||
return html`
|
||||
<frigate-card-menu
|
||||
${ref(this._refMenu)}
|
||||
slot=${ifDefined(slot)}
|
||||
.hass=${this._hass}
|
||||
.menuConfig=${this._config.menu}
|
||||
.buttons=${this._menuButtonController.calculateButtons(
|
||||
@@ -215,6 +283,25 @@ class FrigateCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderStatusBar(slot?: string): TemplateResult | void {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
return html`
|
||||
<frigate-card-status-bar
|
||||
slot=${ifDefined(slot)}
|
||||
.items=${this._controller.getStatusBarItemManager().calculateItems({
|
||||
statusConfig: this._config.status_bar,
|
||||
cameraManager: this._controller.getCameraManager(),
|
||||
view: this._controller.getViewManager().getView(),
|
||||
mediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
||||
})}
|
||||
.config=${this._config.status_bar}
|
||||
></frigate-card-status-bar>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
// Execute query string actions after first render is complete.
|
||||
this._controller.getQueryStringManager().executeNonViewRelated();
|
||||
@@ -250,8 +337,6 @@ class FrigateCard extends LitElement {
|
||||
};
|
||||
|
||||
const actions = this._controller.getActionsManager().getMergedActions();
|
||||
const renderMenuAbove =
|
||||
this._config?.menu.style === 'outside' && this._config?.menu.position === 'top';
|
||||
const cameraManager = this._controller.getCameraManager();
|
||||
|
||||
// Caution: Keep the main div and the menu next to one another in order to
|
||||
@@ -281,8 +366,9 @@ class FrigateCard extends LitElement {
|
||||
}
|
||||
@frigate-card:focus=${() => this.focus()}
|
||||
>
|
||||
${renderMenuAbove ? this._renderMenu() : ''}
|
||||
${this._renderMenuStatusContainer('top')}
|
||||
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
|
||||
${this._renderMenuStatusContainer('overlay')}
|
||||
${!cameraManager.isInitialized() &&
|
||||
!this._controller.getMessageManager().hasMessage()
|
||||
? renderProgressIndicator({
|
||||
@@ -320,7 +406,7 @@ class FrigateCard extends LitElement {
|
||||
renderMessage(this._controller.getMessageManager().getMessage())
|
||||
}
|
||||
</div>
|
||||
${!renderMenuAbove ? this._renderMenu() : ''}
|
||||
${this._renderMenuStatusContainer('bottom')}
|
||||
${this._config?.elements
|
||||
? // Elements need to render after the main views so it can render 'on
|
||||
// top'.
|
||||
@@ -331,14 +417,24 @@ class FrigateCard extends LitElement {
|
||||
.conditionsManagerEpoch=${this._controller
|
||||
.getConditionsManager()
|
||||
?.getEpoch()}
|
||||
@frigate-card:menu-add=${(ev: CustomEvent<MenuItem>) => {
|
||||
@frigate-card:menu:add=${(ev: CustomEvent<MenuItem>) => {
|
||||
this._menuButtonController.addDynamicMenuButton(ev.detail);
|
||||
this.requestUpdate();
|
||||
}}
|
||||
@frigate-card:menu-remove=${(ev: CustomEvent<MenuItem>) => {
|
||||
@frigate-card:menu:remove=${(ev: CustomEvent<MenuItem>) => {
|
||||
this._menuButtonController.removeDynamicMenuButton(ev.detail);
|
||||
this.requestUpdate();
|
||||
}}
|
||||
@frigate-card:status-bar:add=${(ev: CustomEvent<StatusBarItem>) => {
|
||||
this._controller
|
||||
.getStatusBarItemManager()
|
||||
.addDynamicStatusBarItem(ev.detail);
|
||||
}}
|
||||
@frigate-card:status-bar:remove=${(ev: CustomEvent<StatusBarItem>) => {
|
||||
this._controller
|
||||
.getStatusBarItemManager()
|
||||
.removeDynamicStatusBarItem(ev.detail);
|
||||
}}
|
||||
@frigate-card:conditions:evaluate=${(
|
||||
ev: ConditionsEvaluateRequestEvent,
|
||||
) => {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MediaTechnology } from '../../../types';
|
||||
import { VideoRTC } from '../../../components/live/go2rtc/video-rtc';
|
||||
|
||||
export const getTechnologyForVideoRTC = (
|
||||
element: VideoRTC,
|
||||
): MediaTechnology[] | undefined => {
|
||||
const tech = [
|
||||
...(!!element.pc ? ['webrtc'] : []),
|
||||
...(!element.pc && element.mseCodecs ? ['mse', 'hls'] : []),
|
||||
];
|
||||
return tech.length ? tech : undefined;
|
||||
};
|
||||
@@ -165,7 +165,7 @@ export class MenuController {
|
||||
}
|
||||
|
||||
public getFreshButtonState(hass: HomeAssistant, button: MenuItem): StateParameters {
|
||||
const stateParameters = { ...button };
|
||||
const stateParameters = { ...button } as StateParameters;
|
||||
return hass && button.type === 'custom:frigate-card-menu-state-icon'
|
||||
? refreshDynamicStateParameters(hass, stateParameters)
|
||||
: stateParameters;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { HASSDomEvent } from '@dermotduffy/custom-card-helpers';
|
||||
import { LitElement } from 'lit';
|
||||
import { isEqual, orderBy } from 'lodash-es';
|
||||
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request';
|
||||
import {
|
||||
ActionsConfig,
|
||||
FRIGATE_STATUS_BAR_PRIORITY_DEFAULT,
|
||||
StatusBarConfig,
|
||||
StatusBarItem,
|
||||
} from '../config/types';
|
||||
import { getActionConfigGivenAction } from '../utils/action';
|
||||
import { arrayify, setOrRemoveAttribute } from '../utils/basic';
|
||||
import { Timer } from '../utils/timer';
|
||||
|
||||
export class StatusBarController {
|
||||
protected _host: LitElement;
|
||||
protected _config: StatusBarConfig | null = null;
|
||||
|
||||
protected _popupTimer = new Timer();
|
||||
protected _items: StatusBarItem[] = [];
|
||||
|
||||
constructor(host: LitElement) {
|
||||
this._host = host;
|
||||
}
|
||||
|
||||
public getRenderItems(): StatusBarItem[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
public setItems(items: StatusBarItem[]): void {
|
||||
const exclusiveItems = items.filter((item) => !!item.exclusive);
|
||||
|
||||
const newItems = orderBy(
|
||||
exclusiveItems.length ? exclusiveItems : items,
|
||||
(item) => item.priority ?? FRIGATE_STATUS_BAR_PRIORITY_DEFAULT,
|
||||
'desc',
|
||||
);
|
||||
|
||||
const sufficientBefore = this._getSufficientValues(this._items);
|
||||
const sufficientAfter = this._getSufficientValues(newItems);
|
||||
|
||||
this._items = newItems;
|
||||
|
||||
if (this._config?.style === 'popup' && !isEqual(sufficientBefore, sufficientAfter)) {
|
||||
this._show();
|
||||
this._popupTimer.start(this._config.popup_seconds, () => this._hide());
|
||||
}
|
||||
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
public setConfig(config: StatusBarConfig): void {
|
||||
this._config = config;
|
||||
this._host.style.setProperty(
|
||||
'--frigate-card-status-bar-height',
|
||||
`${config.height}px`,
|
||||
);
|
||||
|
||||
this._host.setAttribute('data-style', config.style);
|
||||
this._host.setAttribute('data-position', config.position);
|
||||
|
||||
if (this._config?.style !== 'popup') {
|
||||
this._show();
|
||||
}
|
||||
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
public shouldRender(): boolean {
|
||||
return this._items.some((item) => item.enabled !== false && item.sufficient);
|
||||
}
|
||||
|
||||
public actionHandler(
|
||||
ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
|
||||
config?: ActionsConfig,
|
||||
): void {
|
||||
// These interactions should only be handled by the status bar, as nothing
|
||||
// upstream has the user-provided configuration.
|
||||
ev.stopPropagation();
|
||||
|
||||
const interaction: string = ev.detail.action;
|
||||
const action = getActionConfigGivenAction(interaction, config);
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
action: arrayify(action),
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
protected _getSufficientValue(item: StatusBarItem): string | null {
|
||||
/* istanbul ignore else: cannot happen -- @preserve */
|
||||
if (item.type === 'custom:frigate-card-status-bar-icon') {
|
||||
return item.icon;
|
||||
} else if (item.type === 'custom:frigate-card-status-bar-string') {
|
||||
return item.string;
|
||||
} else if (item.type === 'custom:frigate-card-status-bar-image') {
|
||||
return item.image;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected _getSufficientValues(items: StatusBarItem[]): (string | null)[] {
|
||||
return items
|
||||
.filter((item) => item.enabled !== false && item.sufficient)
|
||||
.map((item) => this._getSufficientValue(item));
|
||||
}
|
||||
|
||||
protected _show(): void {
|
||||
setOrRemoveAttribute(this._host, false, 'hide');
|
||||
}
|
||||
|
||||
protected _hide(): void {
|
||||
setOrRemoveAttribute(this._host, true, 'hide');
|
||||
}
|
||||
}
|
||||
+107
-52
@@ -20,6 +20,10 @@ import {
|
||||
MenuSubmenu,
|
||||
MenuSubmenuSelect,
|
||||
PictureElements,
|
||||
StatusBarIcon,
|
||||
StatusBarImage,
|
||||
StatusBarItem,
|
||||
StatusBarString,
|
||||
} from '../config/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import elementsStyle from '../scss/elements.scss';
|
||||
@@ -164,50 +168,66 @@ export class FrigateCardElements extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public elements: PictureElements;
|
||||
|
||||
protected _boundMenuRemoveHandler = this._menuRemoveHandler.bind(this);
|
||||
|
||||
/**
|
||||
* 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).
|
||||
dispatchFrigateCardEvent<MenuItem>(this, 'menu-remove', (ev as CustomEvent).detail);
|
||||
protected _addHandler(
|
||||
target: EventTarget,
|
||||
eventName: string,
|
||||
handler: (ev: Event) => void,
|
||||
) {
|
||||
// Ensure listener is only attached 1 time by removing it first.
|
||||
target.removeEventListener(eventName, handler);
|
||||
target.addEventListener(eventName, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a picture element to be added to the menu.
|
||||
* @param ev The event.
|
||||
*/
|
||||
protected _menuAddHandler(ev: Event): void {
|
||||
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).
|
||||
dispatchFrigateCardEvent<MenuItem>(this, 'menu:remove', (ev as CustomEvent).detail);
|
||||
};
|
||||
|
||||
protected _statusBarRemoveHandler = (ev: Event): void => {
|
||||
// Re-dispatch event from this element (instead of the disconnected one, as
|
||||
// there is no parent of the disconnected element).
|
||||
dispatchFrigateCardEvent<StatusBarItem>(
|
||||
this,
|
||||
'status-bar:remove',
|
||||
(ev as CustomEvent).detail,
|
||||
);
|
||||
};
|
||||
|
||||
protected _menuAddHandler = (ev: Event): void => {
|
||||
ev = ev as CustomEvent<MenuItem>;
|
||||
const path = ev.composedPath();
|
||||
if (!path.length) {
|
||||
return;
|
||||
}
|
||||
this._addHandler(path[0], 'frigate-card:menu:remove', this._menuRemoveHandler);
|
||||
};
|
||||
|
||||
// See 'A note on custom elements' above to explain what's going on here.
|
||||
|
||||
// Ensure listener is only attached 1 time by removing it first.
|
||||
path[0].removeEventListener(
|
||||
'frigate-card:menu-remove',
|
||||
this._boundMenuRemoveHandler,
|
||||
);
|
||||
|
||||
path[0].addEventListener('frigate-card:menu-remove', this._boundMenuRemoveHandler);
|
||||
protected _statusBarAddHandler = (ev: Event): void => {
|
||||
ev = ev as CustomEvent<MenuItem>;
|
||||
const path = ev.composedPath();
|
||||
if (!path.length) {
|
||||
return;
|
||||
}
|
||||
this._addHandler(
|
||||
path[0],
|
||||
'frigate-card:status-bar:add',
|
||||
this._statusBarRemoveHandler,
|
||||
);
|
||||
};
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
// Catch icons being added to the menu (so their removal can be subsequently
|
||||
// handled).
|
||||
this.addEventListener('frigate-card:menu-add', this._menuAddHandler);
|
||||
// Catch icons being added to the menu or status-bar (so their removal can
|
||||
// be subsequently handled).
|
||||
this.addEventListener('frigate-card:menu:add', this._menuAddHandler);
|
||||
this.addEventListener('frigate-card:status-bar:add', this._statusBarAddHandler);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener('frigate-card:menu-add', this._menuAddHandler);
|
||||
this.removeEventListener('frigate-card:menu:add', this._menuAddHandler);
|
||||
this.addEventListener('frigate-card:status-bar:add', this._statusBarAddHandler);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -286,59 +306,94 @@ export class FrigateCardElementsConditional extends LitElement {
|
||||
}
|
||||
|
||||
// A base class for rendering menu icons / menu state icons.
|
||||
export class FrigateCardElementsBaseMenuIcon<T> extends LitElement {
|
||||
@state()
|
||||
protected _config: T | null = null;
|
||||
export class FrigateCardElementsBaseItem<ConfigType> extends LitElement {
|
||||
protected _eventCategory: string;
|
||||
|
||||
/**
|
||||
* Set the card config.
|
||||
* @param config The configuration.
|
||||
*/
|
||||
public setConfig(config: T): void {
|
||||
constructor(eventCategory: string) {
|
||||
super();
|
||||
this._eventCategory = eventCategory;
|
||||
}
|
||||
|
||||
@state()
|
||||
protected _config: ConfigType | null = null;
|
||||
|
||||
public setConfig(config: ConfigType): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connected callback.
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this._config) {
|
||||
dispatchFrigateCardEvent<T>(this, 'menu-add', this._config);
|
||||
dispatchFrigateCardEvent<ConfigType>(
|
||||
this,
|
||||
`${this._eventCategory}:add`,
|
||||
this._config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
if (this._config) {
|
||||
dispatchFrigateCardEvent<T>(this, 'menu-remove', this._config);
|
||||
dispatchFrigateCardEvent<ConfigType>(
|
||||
this,
|
||||
`${this._eventCategory}:remove`,
|
||||
this._config,
|
||||
);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
export class FrigateCardElementsBaseMenuItem<
|
||||
ConfigType,
|
||||
> extends FrigateCardElementsBaseItem<ConfigType> {
|
||||
constructor() {
|
||||
super('menu');
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('frigate-card-menu-icon')
|
||||
export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuIcon<MenuIcon> {}
|
||||
export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuItem<MenuIcon> {}
|
||||
|
||||
@customElement('frigate-card-menu-state-icon')
|
||||
export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuIcon<MenuStateIcon> {}
|
||||
export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuItem<MenuStateIcon> {}
|
||||
|
||||
@customElement('frigate-card-menu-submenu')
|
||||
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
|
||||
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuItem<MenuSubmenu> {}
|
||||
|
||||
@customElement('frigate-card-menu-submenu-select')
|
||||
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
|
||||
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuItem<MenuSubmenuSelect> {}
|
||||
|
||||
export class FrigateCardElementsBaseStatusBarItem<
|
||||
ConfigType,
|
||||
> extends FrigateCardElementsBaseItem<ConfigType> {
|
||||
constructor() {
|
||||
super('status-bar');
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('frigate-card-status-bar-icon')
|
||||
export class FrigateCardElementsStatusBarIcon extends FrigateCardElementsBaseStatusBarItem<StatusBarIcon> {}
|
||||
|
||||
@customElement('frigate-card-status-bar-image')
|
||||
export class FrigateCardElementsStatusBarImage extends FrigateCardElementsBaseStatusBarItem<StatusBarImage> {}
|
||||
|
||||
@customElement('frigate-card-status-bar-string')
|
||||
export class FrigateCardElementsStatusBarString extends FrigateCardElementsBaseStatusBarItem<StatusBarString> {}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-conditional': FrigateCardElementsConditional;
|
||||
'frigate-card-elements': FrigateCardElements;
|
||||
'frigate-card-menu-submenu-select': FrigateCardElementsMenuSubmenuSelect;
|
||||
'frigate-card-menu-submenu': FrigateCardElementsMenuSubmenu;
|
||||
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
|
||||
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
|
||||
'frigate-card-elements-core': FrigateCardElementsCore;
|
||||
|
||||
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
|
||||
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
|
||||
'frigate-card-menu-submenu': FrigateCardElementsMenuSubmenu;
|
||||
'frigate-card-menu-submenu-select': FrigateCardElementsMenuSubmenuSelect;
|
||||
|
||||
'frigate-card-status-bar-icon': FrigateCardElementsStatusBarIcon;
|
||||
'frigate-card-status-bar-image': FrigateCardElementsStatusBarImage;
|
||||
'frigate-card-status-bar-string': FrigateCardElementsStatusBarString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
dispatchMediaPlayEvent,
|
||||
dispatchMediaVolumeChangeEvent,
|
||||
} from '../../../utils/media-info';
|
||||
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
||||
|
||||
/**
|
||||
* Video player for go2rtc streaming application.
|
||||
@@ -325,6 +326,7 @@ export class VideoRTC extends HTMLElement {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(this.video),
|
||||
},
|
||||
technology: getTechnologyForVideoRTC(this),
|
||||
});
|
||||
};
|
||||
this.video.onvolumechange = () => dispatchMediaVolumeChangeEvent(this);
|
||||
@@ -641,6 +643,7 @@ export class VideoRTC extends HTMLElement {
|
||||
receivedFirstFrame = true;
|
||||
dispatchMediaLoadedEvent(this, this.video, {
|
||||
player: this.containingPlayer,
|
||||
technology: ['mjpeg'],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -668,6 +671,7 @@ export class VideoRTC extends HTMLElement {
|
||||
|
||||
dispatchMediaLoadedEvent(this, video2, {
|
||||
player: this.containingPlayer,
|
||||
technology: ['mp4'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
||||
capabilities: {
|
||||
supportsPause: true,
|
||||
},
|
||||
technology: ['jsmpeg'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||
import { getTechnologyForVideoRTC } from '../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
||||
import { CameraConfig, CardWideConfig } from '../../config/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveWebRTCCardStyle from '../../scss/live-webrtc-card.scss';
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
import { screenshotMedia } from '../../utils/screenshot.js';
|
||||
import { renderTask } from '../../utils/task.js';
|
||||
import { dispatchErrorMessageEvent, renderProgressIndicator } from '../message.js';
|
||||
import { VideoRTC } from './go2rtc/video-rtc.js';
|
||||
|
||||
// Create a wrapper for AlexxIT's WebRTC card
|
||||
// - https://github.com/AlexxIT/WebRTC
|
||||
@@ -104,15 +106,16 @@ export class FrigateCardLiveWebRTCCard
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected _getVideoRTC(): VideoRTC | null {
|
||||
return (this.renderRoot?.querySelector('#webrtc') ?? null) as VideoRTC | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying video player.
|
||||
* @returns The player or `null` if not found.
|
||||
*/
|
||||
protected _getPlayer(): HTMLVideoElement | null {
|
||||
const root = this.renderRoot?.querySelector('#webrtc') as
|
||||
| (HTMLElement & { video?: HTMLVideoElement })
|
||||
| null;
|
||||
return root?.video ?? null;
|
||||
return this._getVideoRTC()?.video ?? null;
|
||||
}
|
||||
|
||||
protected async _getWebRTCCardElement(): Promise<
|
||||
@@ -192,6 +195,7 @@ export class FrigateCardLiveWebRTCCard
|
||||
// Extract the video component after it has been rendered and generate the
|
||||
// media load event.
|
||||
this.updateComplete.then(() => {
|
||||
const videoRTC = this._getVideoRTC();
|
||||
const video = this._getPlayer();
|
||||
if (video) {
|
||||
setControlsOnVideo(video, this.controls);
|
||||
@@ -205,6 +209,7 @@ export class FrigateCardLiveWebRTCCard
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(video),
|
||||
},
|
||||
...(videoRTC && { technology: getTechnologyForVideoRTC(videoRTC) }),
|
||||
});
|
||||
};
|
||||
video.onplay = () => dispatchMediaPlayEvent(this);
|
||||
|
||||
@@ -62,11 +62,6 @@ import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import { FrigateCardPTZ } from '../ptz.js';
|
||||
import '../surround.js';
|
||||
import '../title-control.js';
|
||||
import {
|
||||
FrigateCardTitleControl,
|
||||
getDefaultTitleConfigForView,
|
||||
} from '../title-control.js';
|
||||
|
||||
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
||||
|
||||
@@ -294,7 +289,6 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
|
||||
// Index between camera name and slide number.
|
||||
protected _cameraToSlide: Record<string, number> = {};
|
||||
protected _refTitleControl: Ref<FrigateCardTitleControl> = createRef();
|
||||
protected _refPTZControl: Ref<FrigateCardPTZ> = createRef();
|
||||
|
||||
@state()
|
||||
@@ -540,18 +534,10 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
? this.cameraManager.getCameraMetadata(this._getSubstreamCameraID(prevID, view))
|
||||
: null;
|
||||
const cameraID = this.viewFilterCameraID ?? view.camera;
|
||||
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
|
||||
this._getSubstreamCameraID(cameraID, view),
|
||||
);
|
||||
const cameraMetadataNext = nextID
|
||||
? this.cameraManager.getCameraMetadata(this._getSubstreamCameraID(nextID, view))
|
||||
: null;
|
||||
|
||||
const titleConfig = getDefaultTitleConfigForView(
|
||||
view,
|
||||
this.overriddenLiveConfig?.controls.title,
|
||||
);
|
||||
|
||||
// Notes on the below:
|
||||
// - guard() is used to avoid reseting the carousel unless the
|
||||
// options/plugins actually change.
|
||||
@@ -574,9 +560,6 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
transitionEffect=${this._getTransitionEffect()}
|
||||
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
|
||||
@frigate-card:media:loaded=${() => {
|
||||
if (this._refTitleControl.value) {
|
||||
this._refTitleControl.value.show();
|
||||
}
|
||||
this._mediaHasLoaded = true;
|
||||
}}
|
||||
@frigate-card:media:unloaded=${() => {
|
||||
@@ -620,18 +603,6 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
.forceVisibility=${this._mediaHasLoaded && view.context?.ptzControls?.enabled}
|
||||
>
|
||||
</frigate-card-ptz>
|
||||
${cameraMetadataCurrent && titleConfig
|
||||
? html`<frigate-card-title-control
|
||||
${ref(this._refTitleControl)}
|
||||
.config=${titleConfig}
|
||||
.text="${cameraMetadataCurrent
|
||||
? `${localize('common.live')}: ${cameraMetadataCurrent.title}`
|
||||
: ''}"
|
||||
.logo="${cameraMetadataCurrent.engineLogo}"
|
||||
.fitInto=${this as HTMLElement}
|
||||
>
|
||||
</frigate-card-title-control> `
|
||||
: ``}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import overlayStyle from '../scss/overlay.scss';
|
||||
|
||||
@customElement('frigate-card-overlay')
|
||||
export class FrigateCardOverlay extends LitElement {
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<slot name="top"></slot>
|
||||
<slot name="left"></slot>
|
||||
<slot name="right"></slot>
|
||||
<slot name="bottom"></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(overlayStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-overlay': FrigateCardOverlay;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { CSSResultGroup, LitElement, PropertyValues, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
import { StatusBarController } from '../components-lib/status-bar-controller';
|
||||
import { StatusBarConfig, StatusBarItem } from '../config/types';
|
||||
import statusStyle from '../scss/status.scss';
|
||||
import { frigateCardHasAction } from '../utils/action';
|
||||
|
||||
@customElement('frigate-card-status-bar')
|
||||
export class FrigateCardStatusBar extends LitElement {
|
||||
protected _controller = new StatusBarController(this);
|
||||
|
||||
@property({ attribute: false })
|
||||
public items?: StatusBarItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public config?: StatusBarConfig;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
// Always set config before items.
|
||||
if (changedProperties.has('config') && this.config) {
|
||||
this._controller.setConfig(this.config);
|
||||
}
|
||||
|
||||
if (changedProperties.has('items')) {
|
||||
this._controller.setItems(this.items ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._controller.shouldRender()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="status">
|
||||
${this._controller.getRenderItems().map((item): TemplateResult | void => {
|
||||
if (item.enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const classes = classMap({
|
||||
item: true,
|
||||
expand: !!item.expand,
|
||||
action: !!Object.keys(item.actions ?? {}).length,
|
||||
});
|
||||
|
||||
const handler = actionHandler({
|
||||
hasHold: frigateCardHasAction(item.actions?.hold_action),
|
||||
hasDoubleClick: frigateCardHasAction(item.actions?.double_tap_action),
|
||||
});
|
||||
|
||||
if (item.type === 'custom:frigate-card-status-bar-string') {
|
||||
return html`<div
|
||||
.actionHandler=${handler}
|
||||
class="${classes}"
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
>
|
||||
${item.string}
|
||||
</div>`;
|
||||
} else if (item.type === 'custom:frigate-card-status-bar-icon') {
|
||||
return html`<ha-icon
|
||||
.actionHandler=${handler}
|
||||
class="${classes}"
|
||||
icon="${item.icon}"
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
></ha-icon>`;
|
||||
} else if (item.type === 'custom:frigate-card-status-bar-image') {
|
||||
return html`<img
|
||||
.actionHandler=${handler}
|
||||
class="${classes}"
|
||||
src="${item.image}"
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
/>`;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(statusStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-status-bar': FrigateCardStatusBar;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,7 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
if (!this.submenu) {
|
||||
return html``;
|
||||
}
|
||||
const items = this.submenu.items as MenuSubmenuItem[];
|
||||
return html`
|
||||
<ha-button-menu
|
||||
corner=${'BOTTOM_LEFT'}
|
||||
@@ -109,7 +110,7 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
>
|
||||
<ha-icon icon="${this.submenu.icon}"></ha-icon>
|
||||
</ha-icon-button>
|
||||
${this.submenu.items.map(this._renderItem.bind(this))}
|
||||
${items.map(this._renderItem.bind(this))}
|
||||
</ha-button-menu>
|
||||
`;
|
||||
}
|
||||
@@ -217,9 +218,11 @@ export class FrigateCardSubmenuSelect extends LitElement {
|
||||
// the items correctly (below).
|
||||
delete submenu['options'];
|
||||
|
||||
const items = submenu.items as MenuSubmenuItem[];
|
||||
|
||||
for (const option of options) {
|
||||
const title = this._optionTitles?.[option] ?? option;
|
||||
submenu.items.push({
|
||||
items.push({
|
||||
state_color: true,
|
||||
selected: stateObj.state === option,
|
||||
enabled: true,
|
||||
|
||||
@@ -51,10 +51,6 @@ export class FrigateCardSurroundBasic extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
return html` <slot name="above"></slot>
|
||||
<slot></slot>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { TitleControlConfig } from '../config/types';
|
||||
import titleStyle from '../scss/title-control.scss';
|
||||
import { View } from '../view/view.js';
|
||||
|
||||
type PaperToast = HTMLElement & {
|
||||
opened: boolean;
|
||||
};
|
||||
|
||||
export const getDefaultTitleConfigForView = (
|
||||
view?: Readonly<View> | null,
|
||||
baseConfig?: TitleControlConfig,
|
||||
): TitleControlConfig | null => {
|
||||
if (!baseConfig && view?.isGrid()) {
|
||||
return { mode: 'none', duration_seconds: 2 };
|
||||
}
|
||||
return {
|
||||
mode: 'popup-bottom-right',
|
||||
duration_seconds: 2,
|
||||
...baseConfig,
|
||||
};
|
||||
};
|
||||
|
||||
@customElement('frigate-card-title-control')
|
||||
export class FrigateCardTitleControl extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public config?: TitleControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public text?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public fitInto?: HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
public logo?: string;
|
||||
|
||||
protected _toastRef: Ref<PaperToast> = createRef();
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.text ||
|
||||
!this.config ||
|
||||
!this.config.mode ||
|
||||
this.config.duration_seconds === undefined ||
|
||||
this.config.mode === 'none' ||
|
||||
!this.fitInto
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const verticalAlign = this.config.mode.match(/-top-/) ? 'top' : 'bottom';
|
||||
const horizontalAlign = this.config.mode.match(/-left$/) ? 'left' : 'right';
|
||||
|
||||
return html` <paper-toast
|
||||
${ref(this._toastRef)}
|
||||
class="capsule"
|
||||
.duration=${this.config.duration_seconds * 1000}
|
||||
.verticalAlign=${verticalAlign}
|
||||
.horizontalAlign=${horizontalAlign}
|
||||
.text="${this.text}"
|
||||
.fitInto=${this.fitInto}
|
||||
>
|
||||
${this.logo ? html`<img src=${this.logo} />` : ''}
|
||||
</paper-toast>`;
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
return this._toastRef.value?.opened ?? false;
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this._toastRef.value) {
|
||||
// Set it to false first, to ensure the timer resets.
|
||||
this._toastRef.value.opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
if (this._toastRef.value) {
|
||||
// Set it to false first, to ensure the timer resets.
|
||||
this._toastRef.value.opened = false;
|
||||
this._toastRef.value.opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(titleStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-title-control': FrigateCardTitleControl;
|
||||
}
|
||||
}
|
||||
@@ -73,11 +73,6 @@ import type { EmblaCarouselPlugins } from './carousel.js';
|
||||
import './next-prev-control.js';
|
||||
import './ptz';
|
||||
import './surround.js';
|
||||
import './title-control.js';
|
||||
import {
|
||||
FrigateCardTitleControl,
|
||||
getDefaultTitleConfigForView,
|
||||
} from './title-control.js';
|
||||
|
||||
export interface MediaViewerViewContext {
|
||||
seek?: Date;
|
||||
@@ -194,7 +189,6 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
protected _selected = 0;
|
||||
|
||||
protected _media: ViewMedia[] | null = null;
|
||||
protected _refTitleControl: Ref<FrigateCardTitleControl> = createRef();
|
||||
protected _player: FrigateCardMediaPlayer | null = null;
|
||||
|
||||
/**
|
||||
@@ -407,15 +401,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
const cameraMetadata = this.cameraManager.getCameraMetadata(
|
||||
selectedMedia.getCameraID(),
|
||||
);
|
||||
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const titleConfig = getDefaultTitleConfigForView(
|
||||
view,
|
||||
this.viewerConfig?.controls.title,
|
||||
);
|
||||
|
||||
return html`
|
||||
<frigate-card-carousel
|
||||
@@ -427,9 +413,6 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
this._setViewSelectedIndex(ev.detail.index);
|
||||
}}
|
||||
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||
if (this._refTitleControl.value) {
|
||||
this._refTitleControl.value.show();
|
||||
}
|
||||
this._player = ev.detail.player ?? null;
|
||||
this._seekHandler();
|
||||
}}
|
||||
@@ -480,16 +463,6 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
<ha-icon title="${localize('media_viewer.unseekable')}" icon="mdi:clock-remove">
|
||||
</ha-icon>
|
||||
</div>
|
||||
${cameraMetadata && titleConfig
|
||||
? html`<frigate-card-title-control
|
||||
${ref(this._refTitleControl)}
|
||||
.config=${titleConfig}
|
||||
.text="${selectedMedia.getTitle() ?? undefined}"
|
||||
.logo="${cameraMetadata?.engineLogo}"
|
||||
.fitInto=${this as HTMLElement}
|
||||
>
|
||||
</frigate-card-title-control> `
|
||||
: ``}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -919,6 +892,7 @@ export class FrigateCardViewerProvider
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(ev.target as HTMLVideoElement),
|
||||
},
|
||||
technology: ['hls'],
|
||||
});
|
||||
}}
|
||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||
@@ -942,7 +916,7 @@ export class FrigateCardViewerProvider
|
||||
}
|
||||
}}
|
||||
@load=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev, { player: this });
|
||||
dispatchMediaLoadedEvent(this, ev, { player: this, technology: ['jpg'] });
|
||||
}}
|
||||
/>`}
|
||||
`);
|
||||
|
||||
@@ -3,11 +3,6 @@ import get from 'lodash-es/get';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import set from 'lodash-es/set';
|
||||
import unset from 'lodash-es/unset';
|
||||
import {
|
||||
FrigateCardCondition,
|
||||
RawFrigateCardConfig,
|
||||
RawFrigateCardConfigArray,
|
||||
} from './types';
|
||||
import {
|
||||
CONF_AUTOMATIONS,
|
||||
CONF_CAMERAS,
|
||||
@@ -24,18 +19,24 @@ import {
|
||||
CONF_MENU_BUTTONS_CAMERA_UI,
|
||||
CONF_OVERRIDES,
|
||||
CONF_PROFILES,
|
||||
CONF_STATUS_BAR,
|
||||
CONF_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
|
||||
CONF_VIEW_INTERACTION_SECONDS,
|
||||
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
|
||||
CONF_VIEW_DEFAULT_RESET_ENTITIES,
|
||||
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
|
||||
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
|
||||
CONF_VIEW_INTERACTION_SECONDS,
|
||||
CONF_VIEW_TRIGGERS,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
} from '../const';
|
||||
import { arrayify } from '../utils/basic';
|
||||
import {
|
||||
FrigateCardCondition,
|
||||
RawFrigateCardConfig,
|
||||
RawFrigateCardConfigArray,
|
||||
} from './types';
|
||||
|
||||
// *************************************************************************
|
||||
// General Config Management Functions
|
||||
@@ -660,6 +661,27 @@ const ptzControlSettingsTransform = (data: unknown): unknown => {
|
||||
}, {});
|
||||
};
|
||||
|
||||
const titleControlTransform = (data: unknown): unknown => {
|
||||
if (typeof data !== 'object' || !data || typeof data['mode'] !== 'string') {
|
||||
return null;
|
||||
}
|
||||
if (data['mode'] === 'none') {
|
||||
return {
|
||||
style: 'none',
|
||||
};
|
||||
}
|
||||
if (data['mode'].includes('bottom')) {
|
||||
return {
|
||||
position: 'bottom',
|
||||
};
|
||||
} else if (data['mode'].includes('top')) {
|
||||
return {
|
||||
position: 'top',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const UPGRADES = [
|
||||
// v4.0.0 -> v4.1.0
|
||||
upgradeArrayOfObjects(
|
||||
@@ -822,4 +844,9 @@ const UPGRADES = [
|
||||
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
|
||||
),
|
||||
upgradeMoveToWithOverrides('view.update_entities', CONF_VIEW_DEFAULT_RESET_ENTITIES),
|
||||
upgradeMoveTo('live.controls.title', CONF_STATUS_BAR, {
|
||||
transform: titleControlTransform,
|
||||
}),
|
||||
deleteWithOverrides('live.controls.title'),
|
||||
deleteWithOverrides('media_viewer.controls.title'),
|
||||
];
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||
CONF_LIVE_CONTROLS_TITLE_MODE,
|
||||
CONF_LIVE_DRAGGABLE,
|
||||
CONF_LIVE_LAZY_UNLOAD,
|
||||
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
|
||||
CONF_MEDIA_VIEWER_DRAGGABLE,
|
||||
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
|
||||
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||
@@ -40,6 +38,7 @@ import {
|
||||
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
|
||||
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
|
||||
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
|
||||
CONF_STATUS_BAR_STYLE,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
|
||||
@@ -83,8 +82,8 @@ export const LOW_PERFORMANCE_PROFILE = {
|
||||
// Media player next/previous are chevrons.
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE]: 'chevrons' as const,
|
||||
|
||||
[CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE]: 'none' as const,
|
||||
[CONF_LIVE_CONTROLS_TITLE_MODE]: 'none' as const,
|
||||
// Disable the status bar.
|
||||
[CONF_STATUS_BAR_STYLE]: 'none' as const,
|
||||
|
||||
// Move the menu to outside to remove the need to interact with it with open.
|
||||
[CONF_MENU_STYLE]: 'outside',
|
||||
|
||||
+135
-23
@@ -21,12 +21,15 @@ import { PTZ_ACTIONS } from './ptz';
|
||||
// Common Configuration Constants
|
||||
// *************************************************************************
|
||||
|
||||
// The min allowed size of buttons.
|
||||
export const BUTTON_SIZE_MIN = 20;
|
||||
export const STATUS_BAR_HEIGHT_MIN = BUTTON_SIZE_MIN;
|
||||
|
||||
const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
|
||||
export const FRIGATE_MENU_PRIORITY_MAX = 100;
|
||||
|
||||
export const FRIGATE_STATUS_BAR_PRIORITY_DEFAULT = FRIGATE_MENU_PRIORITY_DEFAULT;
|
||||
export const FRIGATE_STATUS_BAR_PRIORITY_MAX = FRIGATE_MENU_PRIORITY_MAX;
|
||||
|
||||
export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
|
||||
'diagnostics',
|
||||
'live',
|
||||
@@ -343,6 +346,19 @@ const sleepActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||
});
|
||||
export type SleepActionConfig = z.infer<typeof sleepActionConfigSchema>;
|
||||
|
||||
const statusBarActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||
frigate_card_action: z.literal('status_bar'),
|
||||
status_bar_action: z.enum(['add', 'remove', 'reset']),
|
||||
|
||||
// This needs to be lazily evaluated since statusBarItemSchema may itself
|
||||
// contain actions.
|
||||
items: z
|
||||
.lazy(() => statusBarItemSchema)
|
||||
.array()
|
||||
.optional(),
|
||||
});
|
||||
export type StatusBarActionConfig = z.infer<typeof statusBarActionConfigSchema>;
|
||||
|
||||
const LOG_ACTIONS_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
|
||||
export type LogActionLevel = (typeof LOG_ACTIONS_LEVELS)[number];
|
||||
|
||||
@@ -366,6 +382,7 @@ export const frigateCardCustomActionSchema = z.union([
|
||||
viewActionConfigSchema,
|
||||
viewDisplayModeActionConfigSchema,
|
||||
sleepActionConfigSchema,
|
||||
statusBarActionConfigSchema,
|
||||
]);
|
||||
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
||||
|
||||
@@ -581,6 +598,52 @@ const menuSubmenuSelectSchema = menuBaseSchema.merge(stateIconSchema).extend({
|
||||
export type MenuSubmenuSelect = z.infer<typeof menuSubmenuSelectSchema>;
|
||||
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelect;
|
||||
|
||||
// *************************************************************************
|
||||
// Custom Element Configuration: Status bar
|
||||
// *************************************************************************
|
||||
|
||||
const statusBarItemBaseSchema = z.object({
|
||||
enabled: z.boolean().default(true).optional(),
|
||||
priority: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(FRIGATE_STATUS_BAR_PRIORITY_MAX)
|
||||
.default(FRIGATE_STATUS_BAR_PRIORITY_DEFAULT)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const statusBarItemElementsBaseSchema = statusBarItemBaseSchema.extend({
|
||||
sufficient: z.boolean().default(false).optional(),
|
||||
exclusive: z.boolean().default(false).optional(),
|
||||
expand: z.boolean().default(false).optional(),
|
||||
actions: actionsBaseSchema.optional(),
|
||||
});
|
||||
|
||||
const statusBarIconItemSchema = statusBarItemElementsBaseSchema.extend({
|
||||
type: z.literal('custom:frigate-card-status-bar-icon'),
|
||||
icon: z.string(),
|
||||
});
|
||||
export type StatusBarIcon = z.infer<typeof statusBarIconItemSchema>;
|
||||
|
||||
const statusBarImageItemSchema = statusBarItemElementsBaseSchema.extend({
|
||||
type: z.literal('custom:frigate-card-status-bar-image'),
|
||||
image: z.string(),
|
||||
});
|
||||
export type StatusBarImage = z.infer<typeof statusBarImageItemSchema>;
|
||||
|
||||
const statusBarStringItemSchema = statusBarItemElementsBaseSchema.extend({
|
||||
type: z.literal('custom:frigate-card-status-bar-string'),
|
||||
string: z.string(),
|
||||
});
|
||||
export type StatusBarString = z.infer<typeof statusBarStringItemSchema>;
|
||||
|
||||
const statusBarItemSchema = z.union([
|
||||
statusBarIconItemSchema,
|
||||
statusBarImageItemSchema,
|
||||
statusBarStringItemSchema,
|
||||
]);
|
||||
export type StatusBarItem = z.infer<typeof statusBarItemSchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Custom Element Configuration: Conditions
|
||||
// *************************************************************************
|
||||
@@ -680,6 +743,9 @@ const pictureElementSchema = z.union([
|
||||
stateBadgeIconSchema,
|
||||
stateIconSchema,
|
||||
stateLabelSchema,
|
||||
statusBarIconItemSchema,
|
||||
statusBarImageItemSchema,
|
||||
statusBarStringItemSchema,
|
||||
]);
|
||||
const pictureElementsSchema = pictureElementSchema.array().optional();
|
||||
export type PictureElements = z.infer<typeof pictureElementsSchema>;
|
||||
@@ -1009,24 +1075,6 @@ const nextPreviousControlConfigSchema = z.object({
|
||||
});
|
||||
export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Title Control Configuration
|
||||
// *************************************************************************
|
||||
|
||||
const titleControlConfigSchema = z.object({
|
||||
mode: z
|
||||
.enum([
|
||||
'none',
|
||||
'popup-top-right',
|
||||
'popup-top-left',
|
||||
'popup-bottom-right',
|
||||
'popup-bottom-left',
|
||||
])
|
||||
.optional(),
|
||||
duration_seconds: z.number().min(0).max(60).optional(),
|
||||
});
|
||||
export type TitleControlConfig = z.infer<typeof titleControlConfigSchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Carousel Transition Configuration
|
||||
// *************************************************************************
|
||||
@@ -1188,7 +1236,6 @@ const liveConfigSchema = z
|
||||
liveConfigDefault.controls.thumbnails,
|
||||
),
|
||||
timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline),
|
||||
title: titleControlConfigSchema.optional(),
|
||||
})
|
||||
.default(liveConfigDefault.controls),
|
||||
display: viewDisplaySchema,
|
||||
@@ -1584,6 +1631,65 @@ export const menuConfigSchema = z
|
||||
.default(menuConfigDefault);
|
||||
export type MenuConfig = z.infer<typeof menuConfigSchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Status Bar Configuration
|
||||
// *************************************************************************
|
||||
|
||||
const STATUS_BAR_STYLES = [
|
||||
'none',
|
||||
'overlay',
|
||||
'hover',
|
||||
'hover-card',
|
||||
'outside',
|
||||
'popup',
|
||||
] as const;
|
||||
|
||||
const STATUS_BAR_POSITIONS = ['top', 'bottom'] as const;
|
||||
|
||||
const statusBarItemDefault = {
|
||||
priority: FRIGATE_STATUS_BAR_PRIORITY_DEFAULT,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const statusBarConfigDefault = {
|
||||
height: 46,
|
||||
items: {
|
||||
engine: statusBarItemDefault,
|
||||
resolution: statusBarItemDefault,
|
||||
technology: statusBarItemDefault,
|
||||
title: statusBarItemDefault,
|
||||
},
|
||||
position: 'bottom' as const,
|
||||
style: 'popup' as const,
|
||||
popup_seconds: 3,
|
||||
};
|
||||
|
||||
export const statusBarConfigSchema = z
|
||||
.object({
|
||||
position: z.enum(STATUS_BAR_POSITIONS).default(statusBarConfigDefault.position),
|
||||
style: z.enum(STATUS_BAR_STYLES).default(statusBarConfigDefault.style),
|
||||
popup_seconds: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(60)
|
||||
.default(statusBarConfigDefault.popup_seconds),
|
||||
height: z.number().min(STATUS_BAR_HEIGHT_MIN).default(statusBarConfigDefault.height),
|
||||
items: z
|
||||
.object({
|
||||
engine: statusBarItemBaseSchema.default(statusBarConfigDefault.items.engine),
|
||||
technology: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.technology,
|
||||
),
|
||||
resolution: statusBarItemBaseSchema.default(
|
||||
statusBarConfigDefault.items.resolution,
|
||||
),
|
||||
title: statusBarItemBaseSchema.default(statusBarConfigDefault.items.title),
|
||||
})
|
||||
.default(statusBarConfigDefault.items),
|
||||
})
|
||||
.default(statusBarConfigDefault);
|
||||
export type StatusBarConfig = z.infer<typeof statusBarConfigSchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Event Viewer Configuration
|
||||
// *************************************************************************
|
||||
@@ -1672,7 +1778,6 @@ const viewerConfigSchema = z
|
||||
timeline: miniTimelineConfigSchema.default(
|
||||
viewerConfigDefault.controls.timeline,
|
||||
),
|
||||
title: titleControlConfigSchema.optional(),
|
||||
})
|
||||
.default(viewerConfigDefault.controls),
|
||||
})
|
||||
@@ -1767,11 +1872,16 @@ export type Overrides = z.infer<typeof overridesSchema>;
|
||||
const automationActionSchema = actionSchema.array();
|
||||
export type AutomationActions = z.infer<typeof automationActionSchema>;
|
||||
|
||||
const automationSchema = z.object({
|
||||
const automationSchema = z
|
||||
.object({
|
||||
conditions: frigateCardConditionSchema.array(),
|
||||
actions: automationActionSchema.optional(),
|
||||
actions_not: automationActionSchema.optional(),
|
||||
});
|
||||
})
|
||||
.refine(
|
||||
(data) => data.actions?.length || data.actions_not?.length,
|
||||
'Automations must include at least one action',
|
||||
);
|
||||
export type Automation = z.infer<typeof automationSchema>;
|
||||
|
||||
const automationsSchema = automationSchema.array();
|
||||
@@ -1857,6 +1967,7 @@ export const frigateCardConfigSchema = z.object({
|
||||
|
||||
view: viewConfigSchema,
|
||||
menu: menuConfigSchema,
|
||||
status_bar: statusBarConfigSchema,
|
||||
live: liveConfigSchema,
|
||||
media_gallery: galleryConfigSchema,
|
||||
media_viewer: viewerConfigSchema,
|
||||
@@ -1892,6 +2003,7 @@ export const frigateCardConfigDefaults = {
|
||||
cameras: cameraConfigDefault,
|
||||
view: viewConfigDefault,
|
||||
menu: menuConfigDefault,
|
||||
status_bar: statusBarConfigDefault,
|
||||
live: liveConfigDefault,
|
||||
media_gallery: galleryConfigDefault,
|
||||
media_viewer: viewerConfigDefault,
|
||||
|
||||
+8
-8
@@ -216,11 +216,6 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS =
|
||||
`${CONF_MEDIA_VIEWER}.controls.timeline.window_seconds` as const;
|
||||
export const CONF_MEDIA_VIEWER_ZOOMABLE = `${CONF_MEDIA_VIEWER}.zoomable` as const;
|
||||
|
||||
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE =
|
||||
`${CONF_MEDIA_VIEWER}.controls.title.mode` as const;
|
||||
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS =
|
||||
`${CONF_MEDIA_VIEWER}.controls.title.duration_seconds` as const;
|
||||
|
||||
const CONF_LIVE = 'live' as const;
|
||||
export const CONF_LIVE_AUTO_PLAY = `${CONF_LIVE}.auto_play` as const;
|
||||
export const CONF_LIVE_AUTO_PAUSE = `${CONF_LIVE}.auto_pause` as const;
|
||||
@@ -272,9 +267,6 @@ export const CONF_LIVE_CONTROLS_TIMELINE_STYLE =
|
||||
`${CONF_LIVE}.controls.timeline.style` as const;
|
||||
export const CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS =
|
||||
`${CONF_LIVE}.controls.timeline.window_seconds` as const;
|
||||
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
|
||||
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
|
||||
`${CONF_LIVE}.controls.title.duration_seconds` as const;
|
||||
export const CONF_LIVE_DISPLAY_MODE = `${CONF_LIVE}.display.mode` as const;
|
||||
export const CONF_LIVE_DISPLAY_GRID_COLUMNS =
|
||||
`${CONF_LIVE}.display.grid_columns` as const;
|
||||
@@ -336,6 +328,14 @@ export const CONF_MENU_BUTTONS_MEDIA_PLAYER =
|
||||
`${CONF_MENU}.buttons.media_player` as const;
|
||||
export const CONF_MENU_BUTTONS_TIMELINE = `${CONF_MENU}.buttons.timeline` as const;
|
||||
|
||||
export const CONF_STATUS_BAR = 'status_bar' as const;
|
||||
export const CONF_STATUS_BAR_POSITION = `${CONF_STATUS_BAR}.position` as const;
|
||||
export const CONF_STATUS_BAR_STYLE = `${CONF_STATUS_BAR}.style` as const;
|
||||
export const CONF_STATUS_BAR_POPUP_SECONDS = `${CONF_STATUS_BAR}.popup_seconds` as const;
|
||||
export const CONF_STATUS_BAR_HEIGHT = `${CONF_STATUS_BAR}.height` as const;
|
||||
|
||||
export const CONF_STATUS_BAR_ITEMS = `${CONF_STATUS_BAR}.items` as const;
|
||||
|
||||
const CONF_DIMENSIONS = 'dimensions' as const;
|
||||
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
|
||||
export const CONF_DIMENSIONS_ASPECT_RATIO_MODE =
|
||||
|
||||
+101
-108
@@ -11,6 +11,8 @@ import {
|
||||
MOTIONEYE_ICON_SVG_PATH,
|
||||
MOTIONEYE_ICON_SVG_VIEWBOX,
|
||||
} from './camera-manager/motioneye/icon.js';
|
||||
import './components/key-assigner.js';
|
||||
import { KeyboardShortcut } from './config/keyboard-shortcuts.js';
|
||||
import {
|
||||
copyConfig,
|
||||
deleteConfigValue,
|
||||
@@ -23,12 +25,14 @@ import {
|
||||
import { setProfiles } from './config/profiles/index.js';
|
||||
import {
|
||||
BUTTON_SIZE_MIN,
|
||||
FRIGATE_MENU_PRIORITY_MAX,
|
||||
FRIGATE_STATUS_BAR_PRIORITY_MAX,
|
||||
FrigateCardConfig,
|
||||
frigateCardConfigDefaults,
|
||||
FRIGATE_MENU_PRIORITY_MAX,
|
||||
profilesSchema,
|
||||
RawFrigateCardConfig,
|
||||
RawFrigateCardConfigArray,
|
||||
STATUS_BAR_HEIGHT_MIN,
|
||||
THUMBNAIL_WIDTH_MAX,
|
||||
THUMBNAIL_WIDTH_MIN,
|
||||
} from './config/types.js';
|
||||
@@ -112,8 +116,6 @@ import {
|
||||
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_STYLE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
|
||||
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
|
||||
CONF_LIVE_CONTROLS_TITLE_MODE,
|
||||
CONF_LIVE_DISPLAY_GRID_COLUMNS,
|
||||
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
|
||||
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
||||
@@ -154,8 +156,6 @@ import {
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
|
||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
|
||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
|
||||
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
|
||||
@@ -166,8 +166,8 @@ import {
|
||||
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||
CONF_MEDIA_VIEWER_ZOOMABLE,
|
||||
CONF_MENU_ALIGNMENT,
|
||||
CONF_MENU_BUTTONS,
|
||||
CONF_MENU_BUTTON_SIZE,
|
||||
CONF_MENU_BUTTONS,
|
||||
CONF_MENU_POSITION,
|
||||
CONF_MENU_STYLE,
|
||||
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||
@@ -176,6 +176,11 @@ import {
|
||||
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
|
||||
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
|
||||
CONF_PROFILES,
|
||||
CONF_STATUS_BAR_HEIGHT,
|
||||
CONF_STATUS_BAR_ITEMS,
|
||||
CONF_STATUS_BAR_POPUP_SECONDS,
|
||||
CONF_STATUS_BAR_POSITION,
|
||||
CONF_STATUS_BAR_STYLE,
|
||||
CONF_TIMELINE_CLUSTERING_THRESHOLD,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
@@ -190,6 +195,12 @@ import {
|
||||
CONF_VIEW_CAMERA_SELECT,
|
||||
CONF_VIEW_DARK_MODE,
|
||||
CONF_VIEW_DEFAULT,
|
||||
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
|
||||
CONF_VIEW_DEFAULT_RESET,
|
||||
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
|
||||
CONF_VIEW_DEFAULT_RESET_ENTITIES,
|
||||
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
|
||||
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
|
||||
CONF_VIEW_INTERACTION_SECONDS,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED,
|
||||
@@ -200,7 +211,6 @@ import {
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_UP,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_IN,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT,
|
||||
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
|
||||
CONF_VIEW_TRIGGERS,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
|
||||
@@ -209,12 +219,7 @@ import {
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS,
|
||||
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
|
||||
MEDIA_CHUNK_SIZE_MAX,
|
||||
CONF_VIEW_DEFAULT_RESET,
|
||||
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
|
||||
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
|
||||
CONF_VIEW_DEFAULT_RESET_ENTITIES,
|
||||
} from './const.js';
|
||||
import { localize } from './localize/localize.js';
|
||||
import frigate_card_editor_style from './scss/editor.scss';
|
||||
@@ -225,10 +230,7 @@ import {
|
||||
getEntityTitle,
|
||||
sideLoadHomeAssistantElements,
|
||||
} from './utils/ha';
|
||||
import './components/key-assigner.js';
|
||||
import { KeyboardShortcut } from './config/keyboard-shortcuts.js';
|
||||
|
||||
const MENU_BUTTONS = 'buttons';
|
||||
const MENU_CAMERAS = 'cameras';
|
||||
const MENU_CAMERAS_CAPABILITIES = 'cameras.capabilities';
|
||||
const MENU_CAMERAS_CAST = 'cameras.cast';
|
||||
@@ -248,7 +250,6 @@ const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
|
||||
const MENU_LIVE_CONTROLS_PTZ = 'live.controls.ptz';
|
||||
const MENU_LIVE_CONTROLS_THUMBNAILS = 'live.controls.thumbnails';
|
||||
const MENU_LIVE_CONTROLS_TIMELINE = 'live.controls.timeline';
|
||||
const MENU_LIVE_CONTROLS_TITLE = 'live.controls.title';
|
||||
const MENU_LIVE_DISPLAY = 'live.display';
|
||||
const MENU_LIVE_MICROPHONE = 'live.microphone';
|
||||
const MENU_MEDIA_GALLERY_CONTROLS_FILTER = 'media_gallery.controls.filter';
|
||||
@@ -257,14 +258,15 @@ const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
|
||||
const MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS = 'media_viewer.controls.next_previous';
|
||||
const MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS = 'media_viewer.controls.thumbnails';
|
||||
const MENU_MEDIA_VIEWER_CONTROLS_TIMELINE = 'media_viewer.controls.timeline';
|
||||
const MENU_MEDIA_VIEWER_CONTROLS_TITLE = 'media_viewer.controls.title';
|
||||
const MENU_MEDIA_VIEWER_DISPLAY = 'media_viewer.display';
|
||||
const MENU_MENU_BUTTONS = 'menu.buttons';
|
||||
const MENU_OPTIONS = 'options';
|
||||
const MENU_PERFORMANCE_FEATURES = 'performance.features';
|
||||
const MENU_PERFORMANCE_STYLE = 'performance.style';
|
||||
const MENU_STATUS_BAR_ITEMS = 'status_bar.items';
|
||||
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
|
||||
const MENU_VIEW_KEYBOARD_SHORTCUTS = 'view.keyboard_shortcuts';
|
||||
const MENU_VIEW_DEFAULT_RESET = 'view.default_reset';
|
||||
const MENU_VIEW_KEYBOARD_SHORTCUTS = 'view.keyboard_shortcuts';
|
||||
const MENU_VIEW_TRIGGERS = 'view.triggers';
|
||||
const MENU_VIEW_TRIGGERS_ACTIONS = 'view.triggers.actions';
|
||||
|
||||
@@ -303,6 +305,11 @@ const options: EditorOptions = {
|
||||
name: localize('editor.menu'),
|
||||
secondary: localize('editor.menu_secondary'),
|
||||
},
|
||||
status_bar: {
|
||||
icon: 'sign-text',
|
||||
name: localize('editor.status_bar'),
|
||||
secondary: localize('editor.status_bar_secondary'),
|
||||
},
|
||||
live: {
|
||||
icon: 'cctv',
|
||||
name: localize('editor.live'),
|
||||
@@ -503,27 +510,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
},
|
||||
];
|
||||
|
||||
protected _titleModes: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{ value: 'none', label: localize('config.common.controls.title.modes.none') },
|
||||
{
|
||||
value: 'popup-top-left',
|
||||
label: localize('config.common.controls.title.modes.popup-top-left'),
|
||||
},
|
||||
{
|
||||
value: 'popup-top-right',
|
||||
label: localize('config.common.controls.title.modes.popup-top-right'),
|
||||
},
|
||||
{
|
||||
value: 'popup-bottom-left',
|
||||
label: localize('config.common.controls.title.modes.popup-bottom-left'),
|
||||
},
|
||||
{
|
||||
value: 'popup-bottom-right',
|
||||
label: localize('config.common.controls.title.modes.popup-bottom-right'),
|
||||
},
|
||||
];
|
||||
|
||||
protected _transitionEffects: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{ value: 'none', label: localize('config.media_viewer.transition_effects.none') },
|
||||
@@ -831,6 +817,22 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
},
|
||||
];
|
||||
|
||||
protected _statusBarStyles: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{ value: 'hover', label: localize('config.status_bar.styles.hover') },
|
||||
{ value: 'hover-card', label: localize('config.status_bar.styles.hover-card') },
|
||||
{ value: 'none', label: localize('config.status_bar.styles.none') },
|
||||
{ value: 'outside', label: localize('config.status_bar.styles.outside') },
|
||||
{ value: 'overlay', label: localize('config.status_bar.styles.overlay') },
|
||||
{ value: 'popup', label: localize('config.status_bar.styles.popup') },
|
||||
];
|
||||
|
||||
protected _statusBarPositions: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{ value: 'top', label: localize('config.status_bar.positions.top') },
|
||||
{ value: 'bottom', label: localize('config.status_bar.positions.bottom') },
|
||||
];
|
||||
|
||||
public setConfig(config: RawFrigateCardConfig): void {
|
||||
// Note: This does not use Zod to parse the full configuration, so it may be
|
||||
// partially or completely invalid. It's more useful to have a partially
|
||||
@@ -1230,11 +1232,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an editor menu for the card menu buttons.
|
||||
* @param button The name of the button.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected _renderStatusBarItem(item: string): TemplateResult {
|
||||
return html` ${this._putInSubmenu(
|
||||
MENU_STATUS_BAR_ITEMS,
|
||||
item,
|
||||
`config.status_bar.items.${item}`,
|
||||
{ name: 'mdi:feature-search' },
|
||||
html`
|
||||
${this._renderSwitch(
|
||||
`${CONF_STATUS_BAR_ITEMS}.${item}.enabled`,
|
||||
this._defaults.status_bar.items[item]?.enabled ?? true,
|
||||
{
|
||||
label: localize('config.status_bar.items.enabled'),
|
||||
},
|
||||
)}
|
||||
${this._renderNumberInput(`${CONF_STATUS_BAR_ITEMS}.${item}.priority`, {
|
||||
max: FRIGATE_STATUS_BAR_PRIORITY_MAX,
|
||||
default: this._defaults.status_bar.items[item]?.priority,
|
||||
label: localize('config.status_bar.items.priority'),
|
||||
})}
|
||||
`,
|
||||
)}`;
|
||||
}
|
||||
|
||||
protected _renderMenuButton(
|
||||
button: string,
|
||||
additionalOptions?: TemplateResult,
|
||||
@@ -1244,29 +1264,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
{ value: 'matching', label: localize('config.menu.buttons.alignments.matching') },
|
||||
{ value: 'opposing', label: localize('config.menu.buttons.alignments.opposing') },
|
||||
];
|
||||
const submenuClasses = {
|
||||
submenu: true,
|
||||
selected: this._expandedMenus[MENU_BUTTONS] === button,
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="${classMap(submenuClasses)}">
|
||||
<div
|
||||
class="submenu-header"
|
||||
@click=${this._toggleMenu}
|
||||
.domain=${MENU_BUTTONS}
|
||||
.key=${button}
|
||||
>
|
||||
<ha-icon .icon=${'mdi:gesture-tap-button'}></ha-icon>
|
||||
<span
|
||||
>${localize('editor.button') +
|
||||
': ' +
|
||||
localize(`config.${CONF_MENU_BUTTONS}.${button}`)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
${this._expandedMenus[MENU_BUTTONS] === button
|
||||
? html` <div class="values">
|
||||
return html` ${this._putInSubmenu(
|
||||
MENU_MENU_BUTTONS,
|
||||
button,
|
||||
`config.menu.buttons.${button}`,
|
||||
{ name: 'mdi:gesture-tap-button' },
|
||||
html`
|
||||
${this._renderSwitch(
|
||||
`${CONF_MENU_BUTTONS}.${button}.enabled`,
|
||||
this._defaults.menu.buttons[button]?.enabled ?? true,
|
||||
@@ -1290,10 +1294,8 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
label: localize('config.menu.buttons.icon'),
|
||||
})}
|
||||
${additionalOptions}
|
||||
</div>`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1730,34 +1732,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the titles controls.
|
||||
* @param domain The submenu domain.
|
||||
* @param configPathMode Title mode config path.
|
||||
* @param configPathDurationSeconds Title duration seconds config path.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected _renderTitleControls(
|
||||
menuDomain: string,
|
||||
configPathMode: string,
|
||||
configPathDurationSeconds: string,
|
||||
): TemplateResult | void {
|
||||
return this._putInSubmenu(
|
||||
menuDomain,
|
||||
true,
|
||||
'config.common.controls.title.editor_label',
|
||||
{ name: 'mdi:subtitles' },
|
||||
html` ${this._renderOptionSelector(configPathMode, this._titleModes, {
|
||||
label: localize('config.common.controls.title.mode'),
|
||||
})}
|
||||
${this._renderNumberInput(configPathDurationSeconds, {
|
||||
min: 0,
|
||||
max: 60,
|
||||
label: localize('config.common.controls.title.duration_seconds'),
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a camera section.
|
||||
* @param cameras The full array of cameras.
|
||||
@@ -2427,6 +2401,35 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this._renderOptionSetHeader('status_bar')}
|
||||
${this._expandedMenus[MENU_OPTIONS] === 'status_bar'
|
||||
? html`
|
||||
<div class="values">
|
||||
${this._renderOptionSelector(
|
||||
CONF_STATUS_BAR_STYLE,
|
||||
this._statusBarStyles,
|
||||
)}
|
||||
${this._renderOptionSelector(
|
||||
CONF_STATUS_BAR_POSITION,
|
||||
this._statusBarPositions,
|
||||
)}
|
||||
${this._renderNumberInput(CONF_STATUS_BAR_HEIGHT, {
|
||||
min: STATUS_BAR_HEIGHT_MIN,
|
||||
label: localize('config.status_bar.height'),
|
||||
})}
|
||||
${this._renderNumberInput(CONF_STATUS_BAR_POPUP_SECONDS, {
|
||||
min: 0,
|
||||
max: 60,
|
||||
default: this._defaults.status_bar.popup_seconds,
|
||||
label: localize('config.status_bar.popup_seconds'),
|
||||
})}
|
||||
${this._renderStatusBarItem('title') /* */}
|
||||
${this._renderStatusBarItem('resolution') /* */}
|
||||
${this._renderStatusBarItem('technology') /* */}
|
||||
${this._renderStatusBarItem('engine') /* */}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this._renderOptionSetHeader('live')}
|
||||
${this._expandedMenus[MENU_OPTIONS] === 'live'
|
||||
? html`
|
||||
@@ -2521,11 +2524,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
configPathMode: CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||
},
|
||||
)}
|
||||
${this._renderTitleControls(
|
||||
MENU_LIVE_CONTROLS_TITLE,
|
||||
CONF_LIVE_CONTROLS_TITLE_MODE,
|
||||
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
|
||||
)}
|
||||
${this._renderMiniTimeline(
|
||||
MENU_LIVE_CONTROLS_TIMELINE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_MODE,
|
||||
@@ -2708,11 +2706,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
configPathMode: CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||
},
|
||||
)}
|
||||
${this._renderTitleControls(
|
||||
MENU_MEDIA_VIEWER_CONTROLS_TITLE,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
|
||||
)}
|
||||
${this._renderMiniTimeline(
|
||||
MENU_MEDIA_VIEWER_CONTROLS_TIMELINE,
|
||||
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE,
|
||||
|
||||
@@ -210,18 +210,6 @@
|
||||
"seek-in-camera": "Cerca d'escombratge només a la càmera seleccionada",
|
||||
"seek-in-media": "Cerca d'escombratge només dins de l'element multimèdia seleccionat"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"duration_seconds": "Segons per mostrar el títol emergent (0=per sempre)",
|
||||
"editor_label": "Controls del títol emergent",
|
||||
"mode": "Mode de visualització del títol emergent",
|
||||
"modes": {
|
||||
"none": "No es mostra el títol",
|
||||
"popup-bottom-left": "Popup a la part inferior esquerra",
|
||||
"popup-bottom-right": "Popup a la part inferior dreta",
|
||||
"popup-top-left": "Popup a la part superior esquerra",
|
||||
"popup-top-right": "Popup a la part superior dreta"
|
||||
}
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
@@ -427,6 +415,33 @@
|
||||
"low-performance": "",
|
||||
"scrubbing": ""
|
||||
},
|
||||
"status_bar": {
|
||||
"height": "",
|
||||
"items": {
|
||||
"enabled": "",
|
||||
"engine": "",
|
||||
"live_provider": "",
|
||||
"priority": "",
|
||||
"resolution": "",
|
||||
"technology": "",
|
||||
"title": ""
|
||||
},
|
||||
"popup_seconds": "",
|
||||
"position": "",
|
||||
"positions": {
|
||||
"bottom": "",
|
||||
"top": ""
|
||||
},
|
||||
"style": "",
|
||||
"styles": {
|
||||
"hover": "",
|
||||
"hover-card": "",
|
||||
"none": "",
|
||||
"outside": "",
|
||||
"overlay": "",
|
||||
"popup": ""
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "Visualitza les càmeres seleccionades recentment",
|
||||
"dark_mode": "Mode fosc",
|
||||
@@ -533,6 +548,8 @@
|
||||
"performance_secondary": "Opcions de rendiment de la targeta",
|
||||
"profiles": "",
|
||||
"profiles_secondary": "",
|
||||
"status_bar": "",
|
||||
"status_bar_secondary": "",
|
||||
"timeline": "Cronologia",
|
||||
"timeline_secondary": "Opcions de la cronologia d'esdeveniments",
|
||||
"upgrade": "Upgrade",
|
||||
|
||||
@@ -210,18 +210,6 @@
|
||||
"seek-in-camera": "Pan seeks within selected camera only",
|
||||
"seek-in-media": "Pan seeks within selected media item only"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"duration_seconds": "Seconds to display popup title (0=forever)",
|
||||
"editor_label": "Popup Title Controls",
|
||||
"mode": "Popup title display mode",
|
||||
"modes": {
|
||||
"none": "No title display",
|
||||
"popup-bottom-left": "Popup on the bottom left",
|
||||
"popup-bottom-right": "Popup on the bottom right",
|
||||
"popup-top-left": "Popup on the top left",
|
||||
"popup-top-right": "Popup on the top right"
|
||||
}
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
@@ -427,6 +415,33 @@
|
||||
"low-performance": "Low performance",
|
||||
"scrubbing": "Video scrubbing"
|
||||
},
|
||||
"status_bar": {
|
||||
"height": "Status bar height in pixels",
|
||||
"items": {
|
||||
"enabled": "Item enabled",
|
||||
"engine": "Camera engine",
|
||||
"live_provider": "Live provider",
|
||||
"priority": "Item priority",
|
||||
"resolution": "Resolution",
|
||||
"technology": "Technology",
|
||||
"title": "Title"
|
||||
},
|
||||
"popup_seconds": "Status bar popup seconds",
|
||||
"position": "Status bar position",
|
||||
"positions": {
|
||||
"bottom": "Positioned on the bottom",
|
||||
"top": "Positioned on the top"
|
||||
},
|
||||
"style": "Status bar style",
|
||||
"styles": {
|
||||
"hover": "Hover status bar",
|
||||
"hover-card": "Hover status bar (card-wide)",
|
||||
"none": "No status bar",
|
||||
"outside": "Outside status bar",
|
||||
"overlay": "Overlay status bar",
|
||||
"popup": "Popup status bar"
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "View for newly selected cameras",
|
||||
"dark_mode": "Dark mode",
|
||||
@@ -533,6 +548,8 @@
|
||||
"performance_secondary": "Card performance options",
|
||||
"profiles": "Configuration profiles",
|
||||
"profiles_secondary": "Choose pre-configured sets of defaults",
|
||||
"status_bar": "Status bar",
|
||||
"status_bar_secondary": "Status bar look & feel options",
|
||||
"timeline": "Timeline",
|
||||
"timeline_secondary": "Event timeline options",
|
||||
"upgrade": "Upgrade",
|
||||
|
||||
@@ -210,18 +210,6 @@
|
||||
"seek-in-camera": "",
|
||||
"seek-in-media": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"duration_seconds": "Secondes pour afficher le titre de la fenêtre contextuelle (0 = toujours)",
|
||||
"editor_label": "Contrôles de titre contextuel",
|
||||
"mode": "Mode d'affichage du titre contextuel",
|
||||
"modes": {
|
||||
"none": "Pas d'affichage du titre",
|
||||
"popup-bottom-left": "Popup en bas à gauche",
|
||||
"popup-bottom-right": "Popup en bas à droite",
|
||||
"popup-top-left": "Popup en haut à gauche",
|
||||
"popup-top-right": "Popup en haut à droite"
|
||||
}
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
@@ -427,6 +415,33 @@
|
||||
"low-performance": "",
|
||||
"scrubbing": ""
|
||||
},
|
||||
"status_bar": {
|
||||
"height": "",
|
||||
"items": {
|
||||
"enabled": "",
|
||||
"engine": "",
|
||||
"live_provider": "",
|
||||
"priority": "",
|
||||
"resolution": "",
|
||||
"technology": "",
|
||||
"title": ""
|
||||
},
|
||||
"popup_seconds": "",
|
||||
"position": "",
|
||||
"positions": {
|
||||
"bottom": "",
|
||||
"top": ""
|
||||
},
|
||||
"style": "",
|
||||
"styles": {
|
||||
"hover": "",
|
||||
"hover-card": "",
|
||||
"none": "",
|
||||
"outside": "",
|
||||
"overlay": "",
|
||||
"popup": ""
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "Afficher les caméras nouvellement sélectionnées",
|
||||
"dark_mode": "Mode sombre",
|
||||
@@ -533,6 +548,8 @@
|
||||
"performance_secondary": "Options de performances de la carte",
|
||||
"profiles": "",
|
||||
"profiles_secondary": "",
|
||||
"status_bar": "",
|
||||
"status_bar_secondary": "",
|
||||
"timeline": "Chronologie",
|
||||
"timeline_secondary": "Options de chronologie des événements",
|
||||
"upgrade": "Mise à niveau",
|
||||
|
||||
@@ -210,18 +210,6 @@
|
||||
"seek-in-camera": "",
|
||||
"seek-in-media": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"duration_seconds": "Secondi per visualizzare il titolo popup (0 = per sempre)",
|
||||
"editor_label": "Controlli titolo popup",
|
||||
"mode": "Modalità di visualizzazione del titolo",
|
||||
"modes": {
|
||||
"none": "Nessuna visualizzazione del titolo",
|
||||
"popup-bottom-left": "Popup in basso a sinistra",
|
||||
"popup-bottom-right": "Popup in basso a destra",
|
||||
"popup-top-left": "Popup in alto a sinistra",
|
||||
"popup-top-right": "Popup in alto a destra"
|
||||
}
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
@@ -427,6 +415,33 @@
|
||||
"low-performance": "",
|
||||
"scrubbing": ""
|
||||
},
|
||||
"status_bar": {
|
||||
"height": "",
|
||||
"items": {
|
||||
"enabled": "",
|
||||
"engine": "",
|
||||
"live_provider": "",
|
||||
"priority": "",
|
||||
"resolution": "",
|
||||
"technology": "",
|
||||
"title": ""
|
||||
},
|
||||
"popup_seconds": "",
|
||||
"position": "",
|
||||
"positions": {
|
||||
"bottom": "",
|
||||
"top": ""
|
||||
},
|
||||
"style": "",
|
||||
"styles": {
|
||||
"hover": "",
|
||||
"hover-card": "",
|
||||
"none": "",
|
||||
"outside": "",
|
||||
"overlay": "",
|
||||
"popup": ""
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "Visualizza per le telecamere appena selezionate",
|
||||
"dark_mode": "Tema scuro",
|
||||
@@ -533,6 +548,8 @@
|
||||
"performance_secondary": "",
|
||||
"profiles": "",
|
||||
"profiles_secondary": "",
|
||||
"status_bar": "",
|
||||
"status_bar_secondary": "",
|
||||
"timeline": "Timeline",
|
||||
"timeline_secondary": "Opzioni della timeline degli eventi",
|
||||
"upgrade": "Aggiornamento",
|
||||
|
||||
@@ -210,18 +210,6 @@
|
||||
"seek-in-camera": "",
|
||||
"seek-in-media": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"duration_seconds": "Segundos para exibir o pop-up (0 = para sempre)",
|
||||
"editor_label": "Controles do pop-up de título",
|
||||
"mode": "Modo de exibição de título de mídia",
|
||||
"modes": {
|
||||
"none": "Sem exibição de título",
|
||||
"popup-bottom-left": "Pop-up no canto inferior esquerdo",
|
||||
"popup-bottom-right": "Pop-up no canto inferior direito",
|
||||
"popup-top-left": "Pop-up no canto superior esquerdo",
|
||||
"popup-top-right": "Pop-up no canto superior direito"
|
||||
}
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
@@ -427,6 +415,33 @@
|
||||
"low-performance": "",
|
||||
"scrubbing": ""
|
||||
},
|
||||
"status_bar": {
|
||||
"height": "",
|
||||
"items": {
|
||||
"enabled": "",
|
||||
"engine": "",
|
||||
"live_provider": "",
|
||||
"priority": "",
|
||||
"resolution": "",
|
||||
"technology": "",
|
||||
"title": ""
|
||||
},
|
||||
"popup_seconds": "",
|
||||
"position": "",
|
||||
"positions": {
|
||||
"bottom": "",
|
||||
"top": ""
|
||||
},
|
||||
"style": "",
|
||||
"styles": {
|
||||
"hover": "",
|
||||
"hover-card": "",
|
||||
"none": "",
|
||||
"outside": "",
|
||||
"overlay": "",
|
||||
"popup": ""
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "Visualização de câmeras recém-selecionadas",
|
||||
"dark_mode": "Modo escuro",
|
||||
@@ -533,6 +548,8 @@
|
||||
"performance_secondary": "Opções de desempenho do cartão",
|
||||
"profiles": "",
|
||||
"profiles_secondary": "",
|
||||
"status_bar": "",
|
||||
"status_bar_secondary": "",
|
||||
"timeline": "Linha do tempo",
|
||||
"timeline_secondary": "Opções do evento da linha do tempo",
|
||||
"upgrade": "Upgrade",
|
||||
|
||||
@@ -210,18 +210,6 @@
|
||||
"seek-in-camera": "",
|
||||
"seek-in-media": ""
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"duration_seconds": "Segundos para exibir o pop-up (0 = para sempre)",
|
||||
"editor_label": "Editor de titulos",
|
||||
"mode": "Modo de exibição de título de mídia",
|
||||
"modes": {
|
||||
"none": "Sem exibição de título",
|
||||
"popup-bottom-left": "Pop-up no canto inferior esquerdo",
|
||||
"popup-bottom-right": "Pop-up no canto inferior direito",
|
||||
"popup-top-left": "Pop-up no canto superior esquerdo",
|
||||
"popup-top-right": "Pop-up no canto superior direito"
|
||||
}
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
@@ -427,6 +415,33 @@
|
||||
"low-performance": "",
|
||||
"scrubbing": ""
|
||||
},
|
||||
"status_bar": {
|
||||
"height": "",
|
||||
"items": {
|
||||
"enabled": "",
|
||||
"engine": "",
|
||||
"live_provider": "",
|
||||
"priority": "",
|
||||
"resolution": "",
|
||||
"technology": "",
|
||||
"title": ""
|
||||
},
|
||||
"popup_seconds": "",
|
||||
"position": "",
|
||||
"positions": {
|
||||
"bottom": "",
|
||||
"top": ""
|
||||
},
|
||||
"style": "",
|
||||
"styles": {
|
||||
"hover": "",
|
||||
"hover-card": "",
|
||||
"none": "",
|
||||
"outside": "",
|
||||
"overlay": "",
|
||||
"popup": ""
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "Visualização de câmeras recém-selecionadas",
|
||||
"dark_mode": "Modo escuro",
|
||||
@@ -533,6 +548,8 @@
|
||||
"performance_secondary": "",
|
||||
"profiles": "",
|
||||
"profiles_secondary": "",
|
||||
"status_bar": "",
|
||||
"status_bar_secondary": "",
|
||||
"timeline": "Linha do tempo",
|
||||
"timeline_secondary": "Opções do evento da linha do tempo",
|
||||
"upgrade": "Actualização",
|
||||
|
||||
@@ -98,7 +98,10 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
||||
return html`
|
||||
<img
|
||||
@load=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev, { player: this });
|
||||
dispatchMediaLoadedEvent(this, ev, {
|
||||
player: this,
|
||||
technology: ['mjpeg'],
|
||||
});
|
||||
}}
|
||||
.src=${typeof this._connected == 'undefined' || this._connected
|
||||
? computeMJPEGStreamUrl(this.stateObj)
|
||||
|
||||
@@ -125,6 +125,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(this._video),
|
||||
},
|
||||
technology: ['hls'],
|
||||
});
|
||||
}}
|
||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||
|
||||
@@ -120,6 +120,7 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(this._video),
|
||||
},
|
||||
technology: ['webrtc'],
|
||||
});
|
||||
}}
|
||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||
|
||||
+49
-17
@@ -80,23 +80,6 @@ div.main.curve-bottom {
|
||||
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
|
||||
/* The 'hover' menu mode is styled applied outside of the menu itself */
|
||||
frigate-card-menu[data-style*='hover'] {
|
||||
z-index: 1;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.main + frigate-card-menu[data-style*='hover'] {
|
||||
opacity: 0;
|
||||
}
|
||||
frigate-card-menu[data-style='hover']:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.main:hover + frigate-card-menu[data-style='hover-card'],
|
||||
frigate-card-menu[data-style='hover-card']:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -181,3 +164,52 @@ web-dialog::part(dialog) {
|
||||
border-radius: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/*************************************
|
||||
* "Outside" style for menu/status bar
|
||||
*************************************/
|
||||
|
||||
.outerlay[data-position='top'] {
|
||||
border-top-left-radius: var(--ha-card-border-radius, 4px);
|
||||
border-top-right-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
|
||||
.outerlay[data-position='bottom'] {
|
||||
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
|
||||
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
|
||||
/****************
|
||||
* Overlay styles
|
||||
****************/
|
||||
frigate-card-overlay {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Menu hover styles
|
||||
*******************/
|
||||
|
||||
frigate-card-menu[data-style*='hover'] {
|
||||
z-index: 4;
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
frigate-card-menu[data-style*='hover']:hover,
|
||||
.main:hover > frigate-card-overlay > frigate-card-menu[data-style='hover-card'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/*************************
|
||||
* Status bar hover styles
|
||||
*************************/
|
||||
|
||||
frigate-card-status-bar[data-style*='hover'] {
|
||||
z-index: 3;
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
frigate-card-status-bar[data-style*='hover']:hover,
|
||||
.main:hover > frigate-card-overlay > frigate-card-status-bar[data-style='hover-card'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
+9
-55
@@ -5,9 +5,7 @@
|
||||
--mdc-icon-button-size: var(--frigate-card-menu-button-size);
|
||||
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
|
||||
|
||||
// Menu itself does not handle click events. Without this, in overlay mode the
|
||||
// menu component prevents clicking on gallery items 'behind' the overlay.
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -45,52 +43,6 @@ div.opposing {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
/************************************
|
||||
* Match menu rounded corners to card
|
||||
************************************/
|
||||
|
||||
:host([data-position='top']),
|
||||
:host([data-position='left']) {
|
||||
border-top-left-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
:host([data-position='top']),
|
||||
:host([data-position='right']) {
|
||||
border-top-right-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
:host([data-position='bottom']),
|
||||
:host([data-position='left']) {
|
||||
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
:host([data-position='bottom']),
|
||||
:host([data-position='right']) {
|
||||
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
|
||||
/**************************************
|
||||
* Positioning for absolute menu styles
|
||||
**************************************/
|
||||
|
||||
:host(:not([data-style='outside'])[data-position='top']),
|
||||
:host(:not([data-style='outside'])[data-position='left'][data-alignment='top']),
|
||||
:host(:not([data-style='outside'])[data-position='right'][data-alignment='top']) {
|
||||
top: 0px;
|
||||
}
|
||||
:host(:not([data-style='outside'])[data-position='bottom']),
|
||||
:host(:not([data-style='outside'])[data-position='left'][data-alignment='bottom']),
|
||||
:host(:not([data-style='outside'])[data-position='right'][data-alignment='bottom']) {
|
||||
bottom: 0px;
|
||||
}
|
||||
:host(:not([data-style='outside'])[data-position='left']),
|
||||
:host(:not([data-style='outside'])[data-position='top'][data-alignment='left']),
|
||||
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='left']) {
|
||||
left: 0px;
|
||||
}
|
||||
:host(:not([data-style='outside'])[data-position='right']),
|
||||
:host(:not([data-style='outside'])[data-position='top'][data-alignment='right']),
|
||||
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='right']) {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Hack: Ensure host & div expand for column flex layouts
|
||||
********************************************************/
|
||||
@@ -156,12 +108,18 @@ div.opposing {
|
||||
:host([data-style='overlay']),
|
||||
:host([data-style*='hover']),
|
||||
:host([data-style='hidden']) {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: calc(var(--frigate-card-menu-button-size) + 6px);
|
||||
height: calc(var(--frigate-card-menu-button-size) + 6px);
|
||||
}
|
||||
|
||||
:host([data-style='overlay']),
|
||||
:host([data-style*='hover']),
|
||||
:host([data-style='hidden'][expanded]) {
|
||||
overflow: visible;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
:host([data-style='overlay'][data-position='top']),
|
||||
:host([data-style='overlay'][data-position='bottom']),
|
||||
:host([data-style*='hover'][data-position='top']),
|
||||
@@ -170,8 +128,6 @@ div.opposing {
|
||||
:host([data-style='hidden'][data-position='bottom'][expanded]) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
:host([data-style='overlay'][data-position='left']),
|
||||
@@ -180,8 +136,6 @@ div.opposing {
|
||||
:host([data-style*='hover'][data-position='right']),
|
||||
:host([data-style='hidden'][data-position='left'][expanded]),
|
||||
:host([data-style='hidden'][data-position='right'][expanded]) {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@use './button.scss';
|
||||
|
||||
:host {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Overlay itself does not handle click events.
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
slot {
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/***************
|
||||
* Slot position
|
||||
***************/
|
||||
|
||||
slot[name='top'] {
|
||||
top: 0px;
|
||||
}
|
||||
slot[name='bottom'] {
|
||||
bottom: 0px;
|
||||
}
|
||||
slot[name='left'] {
|
||||
left: 0px;
|
||||
}
|
||||
slot[name='right'] {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
/***********
|
||||
* Slot size
|
||||
***********/
|
||||
|
||||
slot[name='top'],
|
||||
slot[name='bottom'] {
|
||||
width: 100%;
|
||||
}
|
||||
slot[name='left'],
|
||||
slot[name='right'] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/*******************************
|
||||
* Match rounded corners to card
|
||||
*******************************/
|
||||
|
||||
slot[name='top'],
|
||||
slot[name='left'] {
|
||||
border-top-left-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
slot[name='top'],
|
||||
slot[name='right'] {
|
||||
border-top-right-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
slot[name='bottom'],
|
||||
slot[name='left'] {
|
||||
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
slot[name='bottom'],
|
||||
slot[name='right'] {
|
||||
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
@use './button.scss';
|
||||
|
||||
:host {
|
||||
--mdc-icon-button-size: calc(var(--frigate-card-status-bar-height) - 6px);
|
||||
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
|
||||
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
pointer-events: auto;
|
||||
|
||||
opacity: 1;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
||||
/***************
|
||||
* Outside style
|
||||
***************/
|
||||
|
||||
:host([data-style='outside']) {
|
||||
color: var(--primary-text-color);
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Non-outside style
|
||||
*******************/
|
||||
|
||||
:host(:not([data-style='outside'])) {
|
||||
color: white;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
|
||||
}
|
||||
:host([data-style='popup'][hide]) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*********************
|
||||
* Status bar contents
|
||||
*********************/
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
height: var(--frigate-card-status-bar-height);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: inline-block;
|
||||
|
||||
margin: 3px;
|
||||
padding: 3px;
|
||||
|
||||
align-content: center;
|
||||
|
||||
opacity: 0.7;
|
||||
}
|
||||
.item:first-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.item:last-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.item.expand {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item.action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img.item {
|
||||
width: var(--mdc-icon-size, 24px);
|
||||
height: var(--mdc-icon-size, 24px);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
:host {
|
||||
--paper-toast-background-color: rgba(0, 0, 0, 0.6);
|
||||
--paper-toast-color: white;
|
||||
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
paper-toast {
|
||||
max-width: unset;
|
||||
min-width: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// Without this the paper-toast will consume vertical space before being
|
||||
// opened, which causes the card to render blank space needlessly. It also
|
||||
// won't work with 'display: none', it appears to need something with
|
||||
// width/height properties even before being opened.
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
paper-toast.paper-toast-open {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
paper-toast img {
|
||||
max-height: 24px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
@@ -31,9 +31,13 @@ export interface MediaLoadedCapabilities {
|
||||
hasAudio?: boolean;
|
||||
}
|
||||
|
||||
const MEDIA_TECHNOLOGY = ['hls', 'jpg', 'jsmpeg', 'mjpeg', 'mp4', 'mse', 'webrtc'];
|
||||
export type MediaTechnology = (typeof MEDIA_TECHNOLOGY)[number];
|
||||
|
||||
export interface MediaLoadedInfo {
|
||||
width: number;
|
||||
height: number;
|
||||
technology?: MediaTechnology[];
|
||||
player?: FrigateCardMediaPlayer;
|
||||
capabilities?: MediaLoadedCapabilities;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
FrigateCardMediaPlayer,
|
||||
MediaLoadedCapabilities,
|
||||
MediaLoadedInfo,
|
||||
MediaTechnology,
|
||||
} from '../types.js';
|
||||
import { dispatchFrigateCardEvent } from './basic.js';
|
||||
|
||||
@@ -18,6 +19,7 @@ export function createMediaLoadedInfo(
|
||||
options?: {
|
||||
player?: FrigateCardMediaPlayer;
|
||||
capabilities?: MediaLoadedCapabilities;
|
||||
technology?: MediaTechnology[];
|
||||
},
|
||||
): MediaLoadedInfo | null {
|
||||
let target: HTMLElement | EventTarget;
|
||||
@@ -61,6 +63,7 @@ export function dispatchMediaLoadedEvent(
|
||||
options?: {
|
||||
player?: FrigateCardMediaPlayer;
|
||||
capabilities?: MediaLoadedCapabilities;
|
||||
technology?: MediaTechnology[];
|
||||
},
|
||||
): void {
|
||||
const mediaLoadedInfo = createMediaLoadedInfo(source, options);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
QueryClassifier,
|
||||
QueryResultClassifier,
|
||||
} from '../../src/camera-manager/manager';
|
||||
import { CameraManagerStore } from '../../src/camera-manager/store';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { StatusBarAction } from '../../../../src/card-controller/actions/actions/status-bar';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
describe('should handle status bar action', () => {
|
||||
it('reset', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new StatusBarAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'status_bar',
|
||||
status_bar_action: 'reset',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getStatusBarItemManager().removeAllDynamicStatusBarItems).toBeCalled();
|
||||
});
|
||||
|
||||
it('add', async () => {
|
||||
const api = createCardAPI();
|
||||
const item = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item',
|
||||
};
|
||||
|
||||
const action = new StatusBarAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'status_bar',
|
||||
status_bar_action: 'add',
|
||||
items: [item],
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getStatusBarItemManager().addDynamicStatusBarItem).toBeCalledWith(item);
|
||||
});
|
||||
|
||||
it('remove', async () => {
|
||||
const api = createCardAPI();
|
||||
const item = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item',
|
||||
};
|
||||
|
||||
const action = new StatusBarAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'status_bar',
|
||||
status_bar_action: 'remove',
|
||||
items: [item],
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(api.getStatusBarItemManager().removeDynamicStatusBarItem).toBeCalledWith(
|
||||
item,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CameraSelectAction } from '../../../src/card-controller/actions/actions/camera-select';
|
||||
import { CameraUIAction } from '../../../src/card-controller/actions/actions/camera-ui';
|
||||
import { DefaultAction } from '../../../src/card-controller/actions/actions/default';
|
||||
@@ -21,6 +21,7 @@ import { PTZDigitalAction } from '../../../src/card-controller/actions/actions/p
|
||||
import { PTZMultiAction } from '../../../src/card-controller/actions/actions/ptz-multi';
|
||||
import { ScreenshotAction } from '../../../src/card-controller/actions/actions/screenshot';
|
||||
import { SleepAction } from '../../../src/card-controller/actions/actions/sleep';
|
||||
import { StatusBarAction } from '../../../src/card-controller/actions/actions/status-bar';
|
||||
import { SubstreamOffAction } from '../../../src/card-controller/actions/actions/substream-off';
|
||||
import { SubstreamOnAction } from '../../../src/card-controller/actions/actions/substream-on';
|
||||
import { SubstreamSelectAction } from '../../../src/card-controller/actions/actions/substream-select';
|
||||
@@ -125,6 +126,10 @@ describe('ActionFactory', () => {
|
||||
[{ frigate_card_action: 'snapshots' as const }, ViewAction],
|
||||
[{ frigate_card_action: 'timeline' as const }, ViewAction],
|
||||
[{ frigate_card_action: 'unmute' as const }, UnmuteAction],
|
||||
[
|
||||
{ frigate_card_action: 'status_bar' as const, status_bar_action: 'reset' },
|
||||
StatusBarAction,
|
||||
],
|
||||
])(
|
||||
'frigate_card_action: $frigate_card_action',
|
||||
(action: Partial<FrigateCardCustomAction>, classObject: object) => {
|
||||
|
||||
@@ -306,28 +306,6 @@ describe('getOverriddenConfig', () => {
|
||||
}),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
// it('with empty value and object', () => {
|
||||
// const manager = new ConditionsManager(createCardAPI());
|
||||
// manager.setState({ fullscreen: true });
|
||||
|
||||
// expect(
|
||||
// getOverriddenConfig(manager, config, {
|
||||
// configOverrides: [
|
||||
// {
|
||||
// delete: [''],
|
||||
// conditions: [
|
||||
// {
|
||||
// condition: 'fullscreen' as const,
|
||||
// fullscreen: true,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// emptyKeyReplaces: true,
|
||||
// }),
|
||||
// ).toEqual({});
|
||||
// });
|
||||
});
|
||||
|
||||
describe('should validate schema', () => {
|
||||
@@ -538,6 +516,11 @@ describe('ConditionsManager', () => {
|
||||
state: 'on',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ViewManager } from '../../src/card-controller/view/view-manager';
|
||||
import { FrigateCardEditor } from '../../src/editor';
|
||||
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
|
||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
||||
import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-manager';
|
||||
|
||||
vi.mock('../../src/camera-manager/manager');
|
||||
vi.mock('../../src/card-controller/actions/actions-manager');
|
||||
@@ -50,6 +51,7 @@ vi.mock('../../src/card-controller/media-player-manager');
|
||||
vi.mock('../../src/card-controller/message-manager');
|
||||
vi.mock('../../src/card-controller/microphone-manager');
|
||||
vi.mock('../../src/card-controller/query-string-manager');
|
||||
vi.mock('../../src/card-controller/status-bar-item-manager');
|
||||
vi.mock('../../src/card-controller/style-manager');
|
||||
vi.mock('../../src/card-controller/triggers-manager');
|
||||
vi.mock('../../src/card-controller/view/view-manager');
|
||||
@@ -247,6 +249,12 @@ describe('CardController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getStatusBarItemManager', () => {
|
||||
expect(createController().getStatusBarItemManager()).toBe(
|
||||
vi.mocked(StatusBarItemManager).mock.instances[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('getStyleManager', () => {
|
||||
expect(createController().getStyleManager()).toBe(
|
||||
vi.mocked(StyleManager).mock.instances[0],
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-manager';
|
||||
import { StatusBarString } from '../../src/config/types';
|
||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||
import {
|
||||
TestViewMedia,
|
||||
createCameraManager,
|
||||
createCardAPI,
|
||||
createStore,
|
||||
createView,
|
||||
} from '../test-utils';
|
||||
|
||||
describe('StatusBarItemManager', () => {
|
||||
const testItem: StatusBarString = {
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: 'test',
|
||||
};
|
||||
|
||||
it('should add', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
manager.addDynamicStatusBarItem(testItem);
|
||||
manager.addDynamicStatusBarItem(testItem);
|
||||
|
||||
expect(manager.calculateItems()).toContain(testItem);
|
||||
expect(manager.calculateItems().find((item) => item === testItem).length === 1);
|
||||
});
|
||||
|
||||
it('should remove', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
manager.addDynamicStatusBarItem(testItem);
|
||||
|
||||
manager.removeDynamicStatusBarItem({ ...testItem });
|
||||
expect(manager.calculateItems()).not.toContain(testItem);
|
||||
|
||||
manager.removeDynamicStatusBarItem({ ...testItem, string: 'not-present' });
|
||||
expect(manager.calculateItems()).not.toContain({
|
||||
...testItem,
|
||||
string: 'not-present',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove all', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
manager.addDynamicStatusBarItem(testItem);
|
||||
manager.removeAllDynamicStatusBarItems();
|
||||
expect(manager.calculateItems()).not.toContain(testItem);
|
||||
});
|
||||
|
||||
describe('should have standard status bar items', () => {
|
||||
describe('should have title', () => {
|
||||
describe('live', () => {
|
||||
it('with metadata', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
},
|
||||
]);
|
||||
const cameraManager = createCameraManager(store);
|
||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
||||
title: 'Camera Title',
|
||||
icon: 'mdi:camera',
|
||||
});
|
||||
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
cameraManager: cameraManager,
|
||||
view: createView({ view: 'live', camera: 'camera-1' }),
|
||||
}),
|
||||
).toContainEqual({
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: 'Camera Title',
|
||||
expand: true,
|
||||
sufficient: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('without metadata', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
const cameraManager = createCameraManager();
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
cameraManager: cameraManager,
|
||||
view: createView({ view: 'live', camera: 'MISSING-CAMERA' }),
|
||||
}),
|
||||
).not.toContainEqual(expect.objectContaining({ sufficient: true }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('media', () => {
|
||||
it('with a title', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const media = [new TestViewMedia({ title: 'Media Title' })];
|
||||
const queryResults = new MediaQueriesResults({
|
||||
results: media,
|
||||
});
|
||||
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
cameraManager: cameraManager,
|
||||
view: view,
|
||||
}),
|
||||
).toContainEqual({
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: 'Media Title',
|
||||
expand: true,
|
||||
sufficient: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('without a title', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
const cameraManager = createCameraManager();
|
||||
|
||||
const media = [new TestViewMedia()];
|
||||
const queryResults = new MediaQueriesResults({
|
||||
results: media,
|
||||
});
|
||||
|
||||
const view = createView({
|
||||
view: 'media',
|
||||
queryResults: queryResults,
|
||||
});
|
||||
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
cameraManager: cameraManager,
|
||||
view: view,
|
||||
}),
|
||||
).not.toContainEqual(expect.objectContaining({ sufficient: true }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should have resolution', () => {
|
||||
it.each([
|
||||
['1080p landscape', '1080p', 1920, 1080],
|
||||
['1080p portrait', '1080p', 1080, 1920],
|
||||
['1080p approximate', '1080p', 1922, 1082],
|
||||
|
||||
['720p landscape', '720p', 1280, 720],
|
||||
['720p portrait', '720p', 720, 1280],
|
||||
['720p approximate', '720p', 1282, 722],
|
||||
|
||||
['VGA landscape', 'VGA', 640, 480],
|
||||
['VGA portrait', 'VGA', 480, 640],
|
||||
['VGA approximate', 'VGA', 642, 482],
|
||||
|
||||
['4K landscape', '4K', 3840, 2160],
|
||||
['4K portrait', '4K', 2160, 3840],
|
||||
['4K approximate', '4K', 3842, 2162],
|
||||
|
||||
['480p landscape', '480p', 720, 480],
|
||||
['480p portrait', '480p', 480, 720],
|
||||
['480p approximate', '480p', 722, 482],
|
||||
|
||||
['576p landscape', '576p', 720, 576],
|
||||
['576p portrait', '576p', 576, 720],
|
||||
['576p approximate', '576p', 722, 578],
|
||||
|
||||
['8K landscape', '8K', 7680, 4320],
|
||||
['8K portrait', '8K', 4320, 7680],
|
||||
['8K approximate', '8K', 7682, 4322],
|
||||
|
||||
['random', '123x456', 123, 456],
|
||||
])(
|
||||
'%s',
|
||||
(_testName: string, expectedName: string, width: number, height: number) => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
mediaLoadedInfo: { width, height },
|
||||
}),
|
||||
).toContainEqual({
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: expectedName,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('should have technology', () => {
|
||||
it('webrtc', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
mediaLoadedInfo: { width: 640, height: 480, technology: ['webrtc'] },
|
||||
}),
|
||||
).toContainEqual({
|
||||
type: 'custom:frigate-card-status-bar-icon' as const,
|
||||
icon: 'mdi:webrtc',
|
||||
});
|
||||
});
|
||||
|
||||
it('non-webrtc', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
mediaLoadedInfo: { width: 640, height: 480, technology: ['hls'] },
|
||||
}),
|
||||
).toContainEqual({
|
||||
type: 'custom:frigate-card-status-bar-string' as const,
|
||||
string: 'HLS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have engine', () => {
|
||||
const manager = new StatusBarItemManager(createCardAPI());
|
||||
const store = createStore([
|
||||
{
|
||||
cameraID: 'camera-1',
|
||||
},
|
||||
]);
|
||||
const cameraManager = createCameraManager(store);
|
||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
||||
title: 'Camera Title',
|
||||
icon: 'mdi:camera',
|
||||
engineLogo: 'IMAGE_LOGO',
|
||||
});
|
||||
|
||||
expect(
|
||||
manager.calculateItems({
|
||||
cameraManager: cameraManager,
|
||||
view: createView({ view: 'live', camera: 'camera-1' }),
|
||||
}),
|
||||
).toContainEqual({
|
||||
type: 'custom:frigate-card-status-bar-image' as const,
|
||||
image: 'IMAGE_LOGO',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getTechnologyForVideoRTC } from '../../../../src/components-lib/live/utils/get-technology-for-video-rtc';
|
||||
import { VideoRTC } from '../../../../src/components/live/go2rtc/video-rtc';
|
||||
import { createLitElement } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('getTechnologyForVideoRTC', () => {
|
||||
it('webrtc', () => {
|
||||
const element = createLitElement() as unknown as VideoRTC;
|
||||
element.pc = {} as unknown as RTCPeerConnection;
|
||||
expect(getTechnologyForVideoRTC(element)).toEqual(['webrtc']);
|
||||
});
|
||||
|
||||
it('mse', () => {
|
||||
const element = createLitElement() as unknown as VideoRTC;
|
||||
element.mseCodecs = 'mp4a';
|
||||
expect(getTechnologyForVideoRTC(element)).toEqual(['mse', 'hls']);
|
||||
});
|
||||
|
||||
it('other', () => {
|
||||
const element = createLitElement() as unknown as VideoRTC;
|
||||
expect(getTechnologyForVideoRTC(element)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { HASSDomEvent, handleActionConfig } from '@dermotduffy/custom-card-helpers';
|
||||
import { handleActionConfig } from '@dermotduffy/custom-card-helpers';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FRIGATE_ICON_SVG_PATH } from '../../src/camera-manager/frigate/icon';
|
||||
import { MenuController } from '../../src/components-lib/menu-controller';
|
||||
import { ActionsConfig, MenuConfig, menuConfigSchema } from '../../src/config/types';
|
||||
import { MenuConfig, menuConfigSchema } from '../../src/config/types';
|
||||
import { StateParameters } from '../../src/types';
|
||||
import { refreshDynamicStateParameters } from '../../src/utils/ha';
|
||||
import { createHASS, createLitElement } from '../test-utils';
|
||||
import { createInteractionEvent, createHASS, createLitElement } from '../test-utils';
|
||||
|
||||
vi.mock('@dermotduffy/custom-card-helpers');
|
||||
vi.mock('../../src/utils/ha');
|
||||
@@ -14,18 +14,6 @@ const createMenuConfig = (config: unknown): MenuConfig => {
|
||||
return menuConfigSchema.parse(config);
|
||||
};
|
||||
|
||||
const createEvent = (
|
||||
action: string,
|
||||
config?: ActionsConfig,
|
||||
): HASSDomEvent<{ action: string; config?: ActionsConfig }> => {
|
||||
return new CustomEvent<{ action: string; config?: ActionsConfig }>('@action', {
|
||||
detail: {
|
||||
action: action,
|
||||
config: config,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('MenuController', () => {
|
||||
const action = {
|
||||
@@ -362,7 +350,7 @@ describe('MenuController', () => {
|
||||
describe('should handle actions', () => {
|
||||
it('should bail without config', () => {
|
||||
const controller = new MenuController(createLitElement());
|
||||
controller.actionHandler(createEvent('tap'));
|
||||
controller.actionHandler(createInteractionEvent('tap'));
|
||||
expect(vi.mocked(handleActionConfig)).not.toBeCalled();
|
||||
});
|
||||
|
||||
@@ -373,7 +361,7 @@ describe('MenuController', () => {
|
||||
|
||||
const controller = new MenuController(host);
|
||||
|
||||
controller.actionHandler(createEvent('tap'), tapActionConfig);
|
||||
controller.actionHandler(createInteractionEvent('tap'), tapActionConfig);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: [action], config: tapActionConfig },
|
||||
@@ -389,7 +377,7 @@ describe('MenuController', () => {
|
||||
|
||||
const controller = new MenuController(host);
|
||||
|
||||
controller.actionHandler(createEvent('tap', tapActionConfig));
|
||||
controller.actionHandler(createInteractionEvent('tap', tapActionConfig));
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: [action], config: tapActionConfig },
|
||||
@@ -404,7 +392,7 @@ describe('MenuController', () => {
|
||||
|
||||
const controller = new MenuController(host);
|
||||
|
||||
controller.actionHandler(createEvent('tap'), tapActionConfigMulti);
|
||||
controller.actionHandler(createInteractionEvent('tap'), tapActionConfigMulti);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -426,7 +414,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(createEvent('tap'), tapActionConfig);
|
||||
controller.actionHandler(createInteractionEvent('tap'), tapActionConfig);
|
||||
expect(controller.isExpanded()).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -442,7 +430,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(createEvent('end_tap'), {
|
||||
controller.actionHandler(createInteractionEvent('end_tap'), {
|
||||
end_tap_action: action,
|
||||
});
|
||||
expect(controller.isExpanded()).toBeFalsy();
|
||||
@@ -462,7 +450,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(createEvent('start_tap'), {
|
||||
controller.actionHandler(createInteractionEvent('start_tap'), {
|
||||
start_tap_action: action,
|
||||
end_tap_action: action,
|
||||
});
|
||||
@@ -481,7 +469,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(false);
|
||||
expect(controller.isExpanded()).toBeFalsy();
|
||||
|
||||
controller.actionHandler(createEvent('tap'), {
|
||||
controller.actionHandler(createInteractionEvent('tap'), {
|
||||
camera_entity: 'foo',
|
||||
tap_action: menuToggleAction,
|
||||
});
|
||||
@@ -490,7 +478,6 @@ describe('MenuController', () => {
|
||||
|
||||
it('when no action is actually taken', () => {
|
||||
const host = createLitElement();
|
||||
const hass = createHASS();
|
||||
const controller = new MenuController(host);
|
||||
controller.setMenuConfig(
|
||||
createMenuConfig({
|
||||
@@ -501,7 +488,7 @@ describe('MenuController', () => {
|
||||
controller.setExpanded(true);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
|
||||
controller.actionHandler(createEvent('end_tap'), tapActionConfig);
|
||||
controller.actionHandler(createInteractionEvent('end_tap'), tapActionConfig);
|
||||
expect(controller.isExpanded()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { StatusBarController } from '../../src/components-lib/status-bar-controller';
|
||||
import { StatusBarConfig, statusBarConfigSchema } from '../../src/config/types';
|
||||
import { setOrRemoveAttribute } from '../../src/utils/basic';
|
||||
import { createInteractionEvent, createLitElement } from '../test-utils';
|
||||
|
||||
const createConfig = (config?: unknown): StatusBarConfig => {
|
||||
return statusBarConfigSchema.parse(config);
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('StatusBarController', () => {
|
||||
describe('should set config', () => {
|
||||
it('should set config', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new StatusBarController(host);
|
||||
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
position: 'top',
|
||||
style: 'hover',
|
||||
height: 50,
|
||||
}),
|
||||
);
|
||||
expect(host.style.getPropertyValue('--frigate-card-status-bar-height')).toBe(
|
||||
'50px',
|
||||
);
|
||||
expect(host.getAttribute('data-style')).toBe('hover');
|
||||
expect(host.getAttribute('data-position')).toBe('top');
|
||||
expect(host.requestUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not hide when not in popup style', () => {
|
||||
const host = createLitElement();
|
||||
setOrRemoveAttribute(host, true, 'hide');
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
position: 'top',
|
||||
style: 'hover',
|
||||
height: 50,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
});
|
||||
|
||||
it('should not show when in popup style', () => {
|
||||
const host = createLitElement();
|
||||
setOrRemoveAttribute(host, true, 'hide');
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
position: 'top',
|
||||
style: 'popup',
|
||||
height: 50,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(host.getAttribute('hide')).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set items', () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('set/get basic items', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new StatusBarController(host);
|
||||
const items = [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Test',
|
||||
},
|
||||
];
|
||||
|
||||
controller.setItems(items);
|
||||
expect(controller.getRenderItems()).toEqual(items);
|
||||
});
|
||||
|
||||
it('should order items', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new StatusBarController(host);
|
||||
const item1 = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 1',
|
||||
priority: 40,
|
||||
};
|
||||
const item2 = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 2',
|
||||
priority: 10,
|
||||
};
|
||||
const item3 = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 3',
|
||||
priority: 60,
|
||||
};
|
||||
const item4 = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 4',
|
||||
priority: undefined,
|
||||
};
|
||||
|
||||
controller.setItems([item1, item2, item3, item4]);
|
||||
expect(controller.getRenderItems()).toEqual([item3, item4, item1, item2]);
|
||||
});
|
||||
|
||||
it('should treat exclusive items exclusively', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new StatusBarController(host);
|
||||
const item1 = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 1',
|
||||
priority: 100,
|
||||
};
|
||||
const exclusiveItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 2',
|
||||
priority: 1,
|
||||
exclusive: true,
|
||||
};
|
||||
|
||||
controller.setItems([item1, exclusiveItem]);
|
||||
expect(controller.getRenderItems()).toEqual([exclusiveItem]);
|
||||
});
|
||||
|
||||
describe('should recognize sufficient items', () => {
|
||||
it('with sufficient item', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new StatusBarController(host);
|
||||
const insufficientItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 1',
|
||||
sufficient: false,
|
||||
};
|
||||
const sufficientItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 2',
|
||||
sufficient: true,
|
||||
};
|
||||
|
||||
controller.setItems([insufficientItem, sufficientItem]);
|
||||
expect(controller.getRenderItems()).toEqual([insufficientItem, sufficientItem]);
|
||||
expect(controller.shouldRender()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('without sufficient item', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new StatusBarController(host);
|
||||
const insufficientItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 1',
|
||||
sufficient: false,
|
||||
};
|
||||
const sufficientItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 2',
|
||||
sufficient: false,
|
||||
};
|
||||
|
||||
controller.setItems([insufficientItem, sufficientItem]);
|
||||
expect(controller.getRenderItems()).toEqual([insufficientItem, sufficientItem]);
|
||||
expect(controller.shouldRender()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should deal with popup styles correctly', () => {
|
||||
it('should show from empty to sufficient', () => {
|
||||
const host = createLitElement();
|
||||
setOrRemoveAttribute(host, true, 'hide');
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
style: 'popup',
|
||||
}),
|
||||
);
|
||||
|
||||
const sufficientItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 1',
|
||||
priority: 100,
|
||||
sufficient: true,
|
||||
};
|
||||
|
||||
controller.setItems([sufficientItem]);
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
});
|
||||
|
||||
it('should not show from empty to insufficient', () => {
|
||||
const host = createLitElement();
|
||||
setOrRemoveAttribute(host, true, 'hide');
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
style: 'popup',
|
||||
}),
|
||||
);
|
||||
|
||||
const insufficientItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 1',
|
||||
priority: 100,
|
||||
sufficient: false,
|
||||
};
|
||||
|
||||
controller.setItems([insufficientItem]);
|
||||
expect(host.getAttribute('hide')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should show from sufficient to different sufficient', () => {
|
||||
const host = createLitElement();
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
style: 'popup',
|
||||
}),
|
||||
);
|
||||
|
||||
const sufficientString = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'String',
|
||||
priority: 100,
|
||||
sufficient: true,
|
||||
};
|
||||
const sufficientIcon = {
|
||||
type: 'custom:frigate-card-status-bar-icon',
|
||||
icon: 'Icon',
|
||||
priority: 100,
|
||||
sufficient: true,
|
||||
};
|
||||
const sufficientImage = {
|
||||
type: 'custom:frigate-card-status-bar-image',
|
||||
image: 'Image',
|
||||
priority: 100,
|
||||
sufficient: true,
|
||||
};
|
||||
|
||||
controller.setItems([sufficientString]);
|
||||
|
||||
// Emulate the popup being hidden.
|
||||
setOrRemoveAttribute(host, true, 'hide');
|
||||
controller.setItems([sufficientIcon]);
|
||||
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
|
||||
// Emulate the popup being hidden.
|
||||
setOrRemoveAttribute(host, true, 'hide');
|
||||
controller.setItems([sufficientImage]);
|
||||
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
});
|
||||
|
||||
it('should hide popup after expiry', () => {
|
||||
const host = createLitElement();
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
style: 'popup',
|
||||
}),
|
||||
);
|
||||
|
||||
const sufficientItem = {
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item 1',
|
||||
priority: 100,
|
||||
sufficient: true,
|
||||
};
|
||||
|
||||
controller.setItems([sufficientItem]);
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(host.getAttribute('hide')).toBe(null);
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(host.getAttribute('hide')).not.toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle actions', () => {
|
||||
it('should bail without action', () => {
|
||||
const host = createLitElement();
|
||||
const handler = vi.fn();
|
||||
host.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
controller.actionHandler(createInteractionEvent('tap'));
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should request action execution', () => {
|
||||
const host = createLitElement();
|
||||
const handler = vi.fn();
|
||||
host.addEventListener('frigate-card:action:execution-request', handler);
|
||||
|
||||
const controller = new StatusBarController(host);
|
||||
|
||||
const action = {
|
||||
action: 'fire-dom-event' as const,
|
||||
};
|
||||
const tapActionConfig = {
|
||||
tap_action: action,
|
||||
};
|
||||
|
||||
controller.actionHandler(createInteractionEvent('tap'), tapActionConfig);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: [action], config: tapActionConfig },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
frigateCardConfigSchema,
|
||||
} from '../../src/config/types';
|
||||
import { getParseErrorPaths } from '../../src/utils/zod';
|
||||
import { update } from 'lodash-es';
|
||||
|
||||
describe('general functions', () => {
|
||||
it('should set value', () => {
|
||||
@@ -3193,5 +3192,116 @@ describe('should handle version specific upgrades', () => {
|
||||
});
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
|
||||
describe('title controls to status bar', () => {
|
||||
it('when mode is none', () => {
|
||||
const config = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{}],
|
||||
live: {
|
||||
controls: {
|
||||
title: {
|
||||
mode: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
media_viewer: {
|
||||
controls: {
|
||||
title: {
|
||||
mode: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config).toEqual({
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{}],
|
||||
live: { controls: {} },
|
||||
media_viewer: { controls: {} },
|
||||
status_bar: {
|
||||
style: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
|
||||
describe('when mode is invalid type', () => {
|
||||
it.each([[{ mode: { should_not_be: 'an object' } }], ['sideways']])(
|
||||
'%s',
|
||||
(mode: unknown) => {
|
||||
const config = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{}],
|
||||
live: {
|
||||
controls: {
|
||||
title: {
|
||||
mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
media_viewer: {
|
||||
controls: {
|
||||
title: {
|
||||
mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config).toEqual({
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{}],
|
||||
live: { controls: {} },
|
||||
media_viewer: { controls: {} },
|
||||
});
|
||||
|
||||
postUpgradeChecks(config);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe.each([['bottom'], ['top']])('on the %s', (position: string) => {
|
||||
it.each([
|
||||
[`popup-${position}-left` as const],
|
||||
[`popup-${position}-right` as const],
|
||||
])('%s', (mode: string) => {
|
||||
const config = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{}],
|
||||
live: {
|
||||
controls: {
|
||||
title: {
|
||||
mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
media_viewer: {
|
||||
controls: {
|
||||
title: {
|
||||
mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config).toEqual({
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{}],
|
||||
live: { controls: {} },
|
||||
media_viewer: { controls: {} },
|
||||
status_bar: {
|
||||
position,
|
||||
},
|
||||
});
|
||||
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ it('low performance profile', () => {
|
||||
'live.controls.thumbnails.show_favorite_control': false,
|
||||
'live.controls.thumbnails.show_timeline_control': false,
|
||||
'live.controls.timeline.show_recordings': false,
|
||||
'live.controls.title.mode': 'none',
|
||||
'live.draggable': false,
|
||||
'live.lazy_unload': 'all',
|
||||
'live.show_image_during_load': false,
|
||||
@@ -32,7 +31,6 @@ it('low performance profile', () => {
|
||||
'media_viewer.controls.thumbnails.show_favorite_control': false,
|
||||
'media_viewer.controls.thumbnails.show_timeline_control': false,
|
||||
'media_viewer.controls.timeline.show_recordings': false,
|
||||
'media_viewer.controls.title.mode': 'none',
|
||||
'media_viewer.draggable': false,
|
||||
'media_viewer.snapshot_click_plays_clip': false,
|
||||
'media_viewer.transition_effect': 'none',
|
||||
@@ -44,6 +42,7 @@ it('low performance profile', () => {
|
||||
'performance.features.media_chunk_size': 10,
|
||||
'performance.style.border_radius': false,
|
||||
'performance.style.box_shadow': false,
|
||||
'status_bar.style': 'none',
|
||||
'timeline.controls.thumbnails.mode': 'none',
|
||||
'timeline.controls.thumbnails.show_details': false,
|
||||
'timeline.controls.thumbnails.show_download_control': false,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
customSchema,
|
||||
dimensionsConfigSchema,
|
||||
frigateCardCustomActionsBaseSchema,
|
||||
frigateCardCustomActionSchema,
|
||||
} from '../../src/config/types';
|
||||
import { createConfig } from '../test-utils';
|
||||
|
||||
@@ -273,6 +274,30 @@ describe('config defaults', () => {
|
||||
box_shadow: true,
|
||||
},
|
||||
},
|
||||
status_bar: {
|
||||
height: 46,
|
||||
items: {
|
||||
engine: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
resolution: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
technology: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
title: {
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
},
|
||||
},
|
||||
popup_seconds: 3,
|
||||
position: 'bottom',
|
||||
style: 'popup',
|
||||
},
|
||||
timeline: {
|
||||
clustering_threshold: 3,
|
||||
controls: {
|
||||
@@ -507,7 +532,7 @@ describe('should convert webrtc card PTZ to Frigate card PTZ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should lazy evaluate', () => {
|
||||
describe('should lazy evaluate schemas', () => {
|
||||
it('conditional picture element', () => {
|
||||
expect(
|
||||
conditionalSchema.parse({
|
||||
@@ -555,6 +580,21 @@ describe('should lazy evaluate', () => {
|
||||
type: 'conditional',
|
||||
});
|
||||
});
|
||||
|
||||
it('status bar actions', () => {
|
||||
const input = {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'status_bar',
|
||||
status_bar_action: 'reset',
|
||||
items: [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-string',
|
||||
string: 'Item',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(frigateCardCustomActionSchema.parse(input)).toEqual(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle custom frigate elements', () => {
|
||||
@@ -621,3 +661,41 @@ it('media viewer should not support microphone based conditions', () => {
|
||||
}),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
describe('automations should require at least one action', () => {
|
||||
it('no action', () => {
|
||||
expect(() =>
|
||||
createConfig({
|
||||
cameras: [{}],
|
||||
automations: [{ conditions: [] }],
|
||||
}),
|
||||
).toThrowError(/Automations must include at least one action/);
|
||||
});
|
||||
|
||||
it('empty actions', () => {
|
||||
expect(() =>
|
||||
createConfig({
|
||||
cameras: [{}],
|
||||
automations: [{ conditions: [], actions: [], actions_not: [] }],
|
||||
}),
|
||||
).toThrowError(/Automations must include at least one action/);
|
||||
});
|
||||
|
||||
it('at least one action', () => {
|
||||
expect(() =>
|
||||
createConfig({
|
||||
cameras: [{}],
|
||||
automations: [
|
||||
{
|
||||
conditions: [],
|
||||
actions: [
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
+22
-3
@@ -1,4 +1,8 @@
|
||||
import { CurrentUser, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import {
|
||||
CurrentUser,
|
||||
HASSDomEvent,
|
||||
HomeAssistant,
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||
import { LitElement } from 'lit';
|
||||
import { expect, vi } from 'vitest';
|
||||
@@ -15,28 +19,31 @@ import {
|
||||
CameraManagerMediaCapabilities,
|
||||
} from '../src/camera-manager/types';
|
||||
import { ActionsManager } from '../src/card-controller/actions/actions-manager';
|
||||
import { DefaultManager } from '../src/card-controller/default-manager';
|
||||
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
||||
import { CameraURLManager } from '../src/card-controller/camera-url-manager';
|
||||
import { CardElementManager } from '../src/card-controller/card-element-manager';
|
||||
import { ConditionsManager } from '../src/card-controller/conditions-manager';
|
||||
import { ConfigManager } from '../src/card-controller/config/config-manager';
|
||||
import { CardController } from '../src/card-controller/controller';
|
||||
import { DefaultManager } from '../src/card-controller/default-manager';
|
||||
import { DownloadManager } from '../src/card-controller/download-manager';
|
||||
import { ExpandManager } from '../src/card-controller/expand-manager';
|
||||
import { FullscreenManager } from '../src/card-controller/fullscreen-manager';
|
||||
import { HASSManager } from '../src/card-controller/hass-manager';
|
||||
import { InitializationManager } from '../src/card-controller/initialization-manager';
|
||||
import { InteractionManager } from '../src/card-controller/interaction-manager';
|
||||
import { KeyboardStateManager } from '../src/card-controller/keyboard-state-manager';
|
||||
import { MediaLoadedInfoManager } from '../src/card-controller/media-info-manager';
|
||||
import { MediaPlayerManager } from '../src/card-controller/media-player-manager';
|
||||
import { MessageManager } from '../src/card-controller/message-manager';
|
||||
import { MicrophoneManager } from '../src/card-controller/microphone-manager';
|
||||
import { QueryStringManager } from '../src/card-controller/query-string-manager';
|
||||
import { StatusBarItemManager } from '../src/card-controller/status-bar-item-manager';
|
||||
import { StyleManager } from '../src/card-controller/style-manager';
|
||||
import { TriggersManager } from '../src/card-controller/triggers-manager';
|
||||
import { ViewManager } from '../src/card-controller/view/view-manager';
|
||||
import {
|
||||
ActionsConfig,
|
||||
CameraConfig,
|
||||
FrigateCardCondition,
|
||||
FrigateCardConfig,
|
||||
@@ -55,7 +62,6 @@ import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
||||
import { View, ViewParameters } from '../src/view/view';
|
||||
import { KeyboardStateManager } from '../src/card-controller/keyboard-state-manager';
|
||||
|
||||
export const createAction = (
|
||||
action: Record<string, unknown>,
|
||||
@@ -450,6 +456,7 @@ export const createCardAPI = (): CardController => {
|
||||
api.getMessageManager.mockReturnValue(mock<MessageManager>());
|
||||
api.getMicrophoneManager.mockReturnValue(mock<MicrophoneManager>());
|
||||
api.getQueryStringManager.mockReturnValue(mock<QueryStringManager>());
|
||||
api.getStatusBarItemManager.mockReturnValue(mock<StatusBarItemManager>());
|
||||
api.getStyleManager.mockReturnValue(mock<StyleManager>());
|
||||
api.getTriggersManager.mockReturnValue(mock<TriggersManager>());
|
||||
api.getViewManager.mockReturnValue(mock<ViewManager>());
|
||||
@@ -473,3 +480,15 @@ export const callHASubscribeMessageHandler = (
|
||||
export const flushPromises = async (): Promise<void> => {
|
||||
await new Promise(process.nextTick);
|
||||
};
|
||||
|
||||
export const createInteractionEvent = (
|
||||
action: string,
|
||||
config?: ActionsConfig,
|
||||
): HASSDomEvent<{ action: string; config?: ActionsConfig }> => {
|
||||
return new CustomEvent<{ action: string; config?: ActionsConfig }>('@action', {
|
||||
detail: {
|
||||
action: action,
|
||||
config: config,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
+1
-2
@@ -33,8 +33,7 @@
|
||||
"ha-selector",
|
||||
"ha-svg-icon",
|
||||
"mwc-button",
|
||||
"mwc-list-item",
|
||||
"paper-toast"
|
||||
"mwc-list-item"
|
||||
],
|
||||
"rules": {
|
||||
"no-unknown-tag-name": "error",
|
||||
|
||||
+4
-1
@@ -14,10 +14,11 @@ const FULL_COVERAGE_FILES_RELATIVE = [
|
||||
'card-controller/**/*.ts',
|
||||
'components-lib/cached-value-controller.ts',
|
||||
'components-lib/key-assigner-controller.ts',
|
||||
'components-lib/live/live-controller.ts',
|
||||
'components-lib/live/**/*.ts',
|
||||
'components-lib/media-filter-controller.ts',
|
||||
'components-lib/menu-button-controller.ts',
|
||||
'components-lib/menu-controller.ts',
|
||||
'components-lib/status-bar-controller.ts',
|
||||
'components-lib/ptz/*.ts',
|
||||
'components-lib/zoom/*.ts',
|
||||
'config/**/*.ts',
|
||||
@@ -74,6 +75,8 @@ export default defineConfig({
|
||||
test: {
|
||||
include: ['tests/**/*.test.ts'],
|
||||
coverage: {
|
||||
exclude: ['docs/**', 'tests/**', '.eslintrc.cjs'],
|
||||
|
||||
// Favor istanbul for coverage over v8 due to better accuracy.
|
||||
provider: 'istanbul',
|
||||
thresholds: {
|
||||
|
||||
Reference in New Issue
Block a user