Merge pull request #208 from dermotduffy/thumbnail-carousel
Add thumbnail carousel to event viewer and live view
This commit is contained in:
@@ -25,6 +25,8 @@ A full-featured Frigate Lovelace card:
|
|||||||
* Support for filtering events by zone and label.
|
* Support for filtering events by zone and label.
|
||||||
* Arbitrary entity access via menu (e.g. motion sensor access).
|
* Arbitrary entity access via menu (e.g. motion sensor access).
|
||||||
* Fullscreen mode.
|
* Fullscreen mode.
|
||||||
|
* Carousel/Swipeable media & thumbnails.
|
||||||
|
* Direct media downloads.
|
||||||
* Lovelace visual editing support.
|
* Lovelace visual editing support.
|
||||||
* Full [Picture Elements](https://www.home-assistant.io/lovelace/picture-elements/) support.
|
* Full [Picture Elements](https://www.home-assistant.io/lovelace/picture-elements/) support.
|
||||||
* Theme friendly.
|
* Theme friendly.
|
||||||
@@ -65,15 +67,20 @@ lovelace:
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
### Basic options
|
### Basic Options
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `camera_entity` | | The optional Frigate camera entity to use in the `frigate` live provider view. Also used to automatically detect the value of `frigate.camera_name`.|
|
| `camera_entity` | | The optional Frigate camera entity to use in the `frigate` live provider view. Also used to automatically detect the value of `frigate.camera_name`.|
|
||||||
|
|
||||||
### Frigate server options
|
### Frigate Server Options
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
frigate:
|
||||||
|
```
|
||||||
|
|
||||||
All variables listed are under a `frigate:` section.
|
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
@@ -83,9 +90,14 @@ All variables listed are under a `frigate:` section.
|
|||||||
| `zone` | | A Frigate zone used to filter events (clips & snapshots), e.g. 'front_door'.|
|
| `zone` | | A Frigate zone used to filter events (clips & snapshots), e.g. 'front_door'.|
|
||||||
| `client_id` | `frigate` | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://blakeblackshear.github.io/frigate/usage/home-assistant/#multiple-instance-support).|
|
| `client_id` | `frigate` | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://blakeblackshear.github.io/frigate/usage/home-assistant/#multiple-instance-support).|
|
||||||
|
|
||||||
### View options
|
### View Options
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
view:
|
||||||
|
```
|
||||||
|
|
||||||
All variables listed are under a `view:` section.
|
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
@@ -93,30 +105,51 @@ All variables listed are under a `view:` section.
|
|||||||
| `timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
| `timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
||||||
| `actions` | | Actions to use for all views, individual actions may be overriden by view-specific actions. See [actions](#actions) below.|
|
| `actions` | | Actions to use for all views, individual actions may be overriden by view-specific actions. See [actions](#actions) below.|
|
||||||
|
|
||||||
### Menu options
|
### Menu Options
|
||||||
|
|
||||||
All variables listed are under a `menu:` section.
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
menu:
|
||||||
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
| `mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
||||||
| `button_size` | `40px` | The size of the menu buttons (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].|
|
| `button_size` | `40px` | The size of the menu buttons [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
||||||
| `buttons.{frigate, live, clips, snapshots, image, download, frigate_ui, fullscreen}` | `true`, except for `image` | Whether or not to show these builtin actions in the card menu. |
|
| `buttons` | | Whether to show or hide built-in buttons. See below. |
|
||||||
| `conditions` | | Condition(s) that must be met in order for the menu to be displayed. These conditions use the same format as the `custom:frigate-card-conditional` card (see [Possible conditions](#frigate-card-conditions) below). If conditions are specified but not met, then the menu is not rendered.|
|
| `conditions` | | Condition(s) that must be met in order for the menu to be displayed. These conditions use the same format as the `custom:frigate-card-conditional` card (see [Possible conditions](#frigate-card-conditions) below). If conditions are specified but not met, then the menu is not rendered.|
|
||||||
|
|
||||||
### Live options
|
#### Menu Options: Buttons
|
||||||
|
|
||||||
All variables listed are under a `live:` section.
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
menu:
|
||||||
|
buttons:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| - | - | - |
|
||||||
|
| `{frigate, live, clips, snapshots, image, download, frigate_ui, fullscreen}` | `true`, except for `image` | Whether or not to show these builtin actions in the card menu. |
|
||||||
|
|
||||||
|
|
||||||
|
### Live Options
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
live:
|
||||||
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `preload` | `false` | Whether or not to preload the live view. Preloading causes the live view to render in the background so it's instantly available when requested. This consumes additional network/CPU resources continually.|
|
| `preload` | `false` | Whether or not to preload the live view. Preloading causes the live view to render in the background so it's instantly available when requested. This consumes additional network/CPU resources continually.|
|
||||||
| `provider` | `frigate` | The means through which the live camera view is displayed. See [Live Provider](#live-provider) below.|
|
| `provider` | `frigate` | The means through which the live camera view is displayed. See [Live Provider](#live-provider) below.|
|
||||||
| `webrtc.url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).|
|
|
||||||
| `webrtc.entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). |
|
|
||||||
| `webrtc.*`| | Any other options in a `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.|
|
|
||||||
| `actions` | | Actions to use for the `live` view. See [actions](#actions) below.|
|
| `actions` | | Actions to use for the `live` view. See [actions](#actions) below.|
|
||||||
| `jsmpeg.options.{audio, video, pauseWhenHidden, disableGl, disableWebAssembly, preserveDrawingBuffer, progressive, throttled, chunkSize, maxAudioLag, videoBufferSize, audioBufferSize}` | | **Advanced users only**: Control the underlying [JSMPEG library options](https://github.com/phoboslab/jsmpeg#usage). This is not necessary for the vast majority of users: only set these flags if you know what you're doing, as you may entirely break video rendering in the card.|
|
| `controls` | | Configuration for the `live` view controls. See below. |
|
||||||
|
| `jsmpeg` | | Configuration for the `frigate-jsmpeg` live provider. See below.|
|
||||||
|
| `webrtc` | | Configuration for the `webrtc` live provider. See below.|
|
||||||
|
|
||||||
#### Available Live Providers
|
#### Available Live Providers
|
||||||
|
|
||||||
@@ -126,39 +159,134 @@ All variables listed are under a `live:` section.
|
|||||||
|`frigate-jsmpeg`|Lower|Low|Builtin|Stream the JSMPEG stream from Frigate (proxied via the Frigate integration). See [note below on the required integration version](#jsmpeg-troubleshooting) for this live provider to function. This is the only live provider that can view the Frigate `birdseye` view.|
|
|`frigate-jsmpeg`|Lower|Low|Builtin|Stream the JSMPEG stream from Frigate (proxied via the Frigate integration). See [note below on the required integration version](#jsmpeg-troubleshooting) for this live provider to function. This is the only live provider that can view the Frigate `birdseye` view.|
|
||||||
|`webrtc`|Lowest|High|Separate installation required|Uses [WebRTC](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc).|
|
|`webrtc`|Lowest|High|Separate installation required|Uses [WebRTC](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc).|
|
||||||
|
|
||||||
### Event Viewer options
|
|
||||||
|
|
||||||
The `event_viewer` is used for viewing all `clip` and `snapshot` media, in a media carousel. All variables listed are under an `event_viewer:` section.
|
#### Live Provider: JSMPEG Configuration
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
live:
|
||||||
|
jsmpeg:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| - | - | - |
|
||||||
|
| `options` | | **Advanced users only**: Control the underlying [JSMPEG library options](https://github.com/phoboslab/jsmpeg#usage). Supports setting these JSMPEG options `{audio, video, pauseWhenHidden, disableGl, disableWebAssembly, preserveDrawingBuffer, progressive, throttled, chunkSize, maxAudioLag, videoBufferSize, audioBufferSize}`. This is not necessary for the vast majority of users: only set these flags if you know what you're doing, as you may entirely break video rendering in the card.|
|
||||||
|
|
||||||
|
#### Live Provider: WebRTC Configuration
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
live:
|
||||||
|
webrtc:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| - | - | - |
|
||||||
|
| `entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). |
|
||||||
|
| `url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).|
|
||||||
|
| `*`| | Any other options in the `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.|
|
||||||
|
|
||||||
|
#### Live Controls: Thumbnails
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
live:
|
||||||
|
controls:
|
||||||
|
thumbnails:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| - | - | - |
|
||||||
|
| `mode` | `none` | Whether to show the thumbnail carousel `below` the media, `above` the media or to hide it entirely (`none`).|
|
||||||
|
| `size` | `100px` | The size of the thumbnails in the thumbnail carousel [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
||||||
|
| `media` | `clips` | Whether to show `clips` or `snapshots` in the thumbnail carousel in the `live` view.|
|
||||||
|
|
||||||
|
### Event Viewer Options
|
||||||
|
|
||||||
|
The `event_viewer` is used for viewing all `clip` and `snapshot` media, in a media carousel.
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
event_viewer:
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
||||||
| `lazy_load` | `true` | Whether or not to lazily load media in the event viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. |
|
| `lazy_load` | `true` | Whether or not to lazily load media in the event viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. |
|
||||||
| `draggable` | `true` | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
| `draggable` | `true` | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
||||||
| `controls.next_previous.style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
| `controls` | | Configuration for the event viewer. See below. |
|
||||||
| `controls.next_previous.size` | `48px` | The size of the next/previous controls (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].|
|
|
||||||
| `actions` | | Actions to use for all views that use the `event_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.|
|
| `actions` | | Actions to use for all views that use the `event_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.|
|
||||||
|
|
||||||
### Event Gallery options
|
#### Event Viewer Controls: Next / Previous
|
||||||
|
|
||||||
The `event_gallery` is used for providing an overview of all `clips` and `snapshots` in a thumbnail gallery. All variables listed are under a `event_gallery:` section.
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
event_viewer:
|
||||||
|
controls:
|
||||||
|
next_previous:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| - | - | - |
|
||||||
|
| `style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
||||||
|
| `size` | `48px` | The size of the next/previous controls [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
||||||
|
|
||||||
|
#### Event Viewer Controls: Thumbnails
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
event_viewer:
|
||||||
|
controls:
|
||||||
|
thumbnails:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| - | - | - |
|
||||||
|
| `mode` | `none` | Whether to show the thumbnail carousel `below` the media, `above` the media or to hide it entirely (`none`).|
|
||||||
|
| `size` | `100px` | The size of the thumbnails in the thumbnail carousel [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
||||||
|
|
||||||
|
### Event Gallery Options
|
||||||
|
|
||||||
|
The `event_gallery` is used for providing an overview of all `clips` and `snapshots` in a thumbnail gallery.
|
||||||
|
|
||||||
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
event_gallery:
|
||||||
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `actions` | | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.|
|
| `actions` | | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.|
|
||||||
|
|
||||||
### Image options
|
### Image Options
|
||||||
|
|
||||||
All variables listed are under a `image:` section.
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image:
|
||||||
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `src` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).|
|
| `src` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).|
|
||||||
| `actions` | | Actions to use for the `image` view. See [actions](#actions) below.|
|
| `actions` | | Actions to use for the `image` view. See [actions](#actions) below.|
|
||||||
|
|
||||||
### Dimension options
|
### Dimension Options
|
||||||
|
|
||||||
All variables listed are under a `dimensions:` section.
|
All configuration is under:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dimensions:
|
||||||
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
@@ -179,7 +307,6 @@ All variables listed are under a `dimensions:` section.
|
|||||||
* `4 / 3` or `4:3`: Default fullscreen ratio.
|
* `4 / 3` or `4:3`: Default fullscreen ratio.
|
||||||
* `<W>/<H>` or `<W>:<H>`: Any arbitrary aspect-ratio.
|
* `<W>/<H>` or `<W>:<H>`: Any arbitrary aspect-ratio.
|
||||||
|
|
||||||
|
|
||||||
<a name="aspect-ratios"></a>
|
<a name="aspect-ratios"></a>
|
||||||
|
|
||||||
#### Aspect Ratio
|
#### Aspect Ratio
|
||||||
@@ -216,7 +343,7 @@ events/snapshots/UI. A perfect combination!
|
|||||||
|
|
||||||
**Note**: WebRTC must be installed and configured separately (see [details](https://github.com/AlexxIT/WebRTC)) before it can be used with this card.
|
**Note**: WebRTC must be installed and configured separately (see [details](https://github.com/AlexxIT/WebRTC)) before it can be used with this card.
|
||||||
|
|
||||||
#### Specifying the WebRTC Camera
|
#### Specifying The WebRTC Camera
|
||||||
|
|
||||||
WebRTC does **not** support use of Frigate-provided camera entities, as it
|
WebRTC does **not** support use of Frigate-provided camera entities, as it
|
||||||
requires an RTSP stream which Frigate does not provide. There are two ways to
|
requires an RTSP stream which Frigate does not provide. There are two ways to
|
||||||
@@ -248,7 +375,7 @@ live:
|
|||||||
|
|
||||||
See [WebRTC configuration](https://github.com/AlexxIT/WebRTC#configuration) for full configuration options.
|
See [WebRTC configuration](https://github.com/AlexxIT/WebRTC#configuration) for full configuration options.
|
||||||
|
|
||||||
## Picture Elements / Menu customizations
|
## Picture Elements / Menu Customizations
|
||||||
|
|
||||||
This card supports the [Picture Elements configuration
|
This card supports the [Picture Elements configuration
|
||||||
syntax](https://www.home-assistant.io/lovelace/picture-elements/) to seamlessly
|
syntax](https://www.home-assistant.io/lovelace/picture-elements/) to seamlessly
|
||||||
@@ -329,7 +456,7 @@ This card supports several different views:
|
|||||||
|`clip`|Shows an event viewer for the most recent clip for this camera/zone/label. Can also be accessed by holding down the `clips` menu icon.|
|
|`clip`|Shows an event viewer for the most recent clip for this camera/zone/label. Can also be accessed by holding down the `clips` menu icon.|
|
||||||
|`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view_timeout`).|
|
|`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view_timeout`).|
|
||||||
|
|
||||||
### Automatic updates in the `clip` or `snapshot` view
|
### Automatic Updates In The `clip` Or `snapshot` View
|
||||||
|
|
||||||
Updates will occur whenever on every change of the state of the `camera_entity`
|
Updates will occur whenever on every change of the state of the `camera_entity`
|
||||||
or any entity configured under `update_entities`. In particular, if the desire is
|
or any entity configured under `update_entities`. In particular, if the desire is
|
||||||
@@ -345,7 +472,7 @@ update_entities:
|
|||||||
|
|
||||||
See the [other options](#other-options) above.
|
See the [other options](#other-options) above.
|
||||||
|
|
||||||
### Getting from a snapshot to a clip
|
### Navigating From A Snapshot To A Clip
|
||||||
|
|
||||||
Clicking on a snapshot will take the user to a clip that was taken at the ~same
|
Clicking on a snapshot will take the user to a clip that was taken at the ~same
|
||||||
time as the snapshot (if any).
|
time as the snapshot (if any).
|
||||||
@@ -398,10 +525,14 @@ This card supports several menu configurations.
|
|||||||
|
|
||||||
## Screenshot: Snapshot / Clip Gallery
|
## Screenshot: Snapshot / Clip Gallery
|
||||||
|
|
||||||
Full viewing of clips:
|
### Full Viewing Of Events
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/gallery.png" alt="Gallery" width="400px">
|
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/gallery.png" alt="Gallery" width="400px">
|
||||||
|
|
||||||
|
### Live Viewing With Thumbnail Carousel
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/live-thumbnails.gif" alt="Live view with event thumbnails" width="400px">
|
||||||
|
|
||||||
## Card Editing
|
## Card Editing
|
||||||
|
|
||||||
This card supports full editing via the Lovelace card editor. Additional arbitrary configuration for WebRTC may be specified in YAML mode.
|
This card supports full editing via the Lovelace card editor. Additional arbitrary configuration for WebRTC may be specified in YAML mode.
|
||||||
@@ -532,7 +663,7 @@ elements:
|
|||||||
|
|
||||||
<a name="frigate-card-conditional-example"></a>
|
<a name="frigate-card-conditional-example"></a>
|
||||||
|
|
||||||
### Restricting icons to certain views
|
### Restricting Icons To Certain Views
|
||||||
|
|
||||||
You can restrict icons to only show for certain [views](#views) using a
|
You can restrict icons to only show for certain [views](#views) using a
|
||||||
`custom:frigate-card-conditional` element (e.g. PTZ controls)
|
`custom:frigate-card-conditional` element (e.g. PTZ controls)
|
||||||
@@ -607,7 +738,7 @@ elements:
|
|||||||
|
|
||||||
<a name="frigate-card-action"></a>
|
<a name="frigate-card-action"></a>
|
||||||
|
|
||||||
### Triggering card actions
|
### Triggering Card Actions
|
||||||
|
|
||||||
You can control the card itself with the `custom:frigate-card-action` action.
|
You can control the card itself with the `custom:frigate-card-action` action.
|
||||||
|
|
||||||
@@ -630,7 +761,7 @@ elements:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Adding card-wide actions
|
### Adding Card-wide Actions
|
||||||
|
|
||||||
You can add actions to the card to be trigger on `tap`, `double_tap` or `hold`. See [actions](#actions) above.
|
You can add actions to the card to be trigger on `tap`, `double_tap` or `hold`. See [actions](#actions) above.
|
||||||
|
|
||||||
@@ -657,7 +788,7 @@ live:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Hiding the menu in certain circumstances
|
### Hiding The Menu In Certain Circumstances
|
||||||
|
|
||||||
You can add conditions to the menu, which will hide the menu unless met.
|
You can add conditions to the menu, which will hide the menu unless met.
|
||||||
|
|
||||||
@@ -684,47 +815,14 @@ menu:
|
|||||||
|
|
||||||
<a name="jsmpeg-troubleshooting"></a>
|
<a name="jsmpeg-troubleshooting"></a>
|
||||||
|
|
||||||
### JSMPEG live camera only shows a 'spinner'
|
### JSMPEG Live Camera Only Shows A 'spinner'
|
||||||
|
|
||||||
You must be using a version of the [Frigate integration](https://github.com/blakeblackshear/frigate-hass-integration) >= 2.1.0
|
You must be using a version of the [Frigate integration](https://github.com/blakeblackshear/frigate-hass-integration) >= 2.1.0
|
||||||
to use JSMPEG proxying. The `frigate-jsmpeg` live provider will not work with earlier
|
to use JSMPEG proxying. The `frigate-jsmpeg` live provider will not work with earlier
|
||||||
integration versions.
|
integration versions.
|
||||||
|
|
||||||
### Fullscreen button does not appear on iPhone
|
### Fullscreen Button Does Not Appear On iPhone
|
||||||
|
|
||||||
Unfortunately, [iOS does not support the Javascript fullscreen
|
Unfortunately, [iOS does not support the Javascript fullscreen
|
||||||
API](https://caniuse.com/fullscreen). As a result, card-level fullscreen support
|
API](https://caniuse.com/fullscreen). As a result, card-level fullscreen support
|
||||||
for the iPhone is not currently possible.
|
for the iPhone is not currently possible.
|
||||||
|
|
||||||
### Failed to fetch
|
|
||||||
|
|
||||||
**Note:** This error should no longer be possible >= v0.1.5 .
|
|
||||||
|
|
||||||
`Failed to fetch` is a generic error indicating your browser (and this card)
|
|
||||||
could not communicate with the Frigate server specified in the card
|
|
||||||
configuration. This could be for any number of reasons (e.g. incorrect URL,
|
|
||||||
incorrect port, broken DNS, etc).
|
|
||||||
|
|
||||||
If the 'globe' icon in the menu bar of the card also doesn't open the Frigate
|
|
||||||
UI, the address entered is probably incorrect/inaccessible.
|
|
||||||
|
|
||||||
#### Mixed content
|
|
||||||
|
|
||||||
**Note:** This error should no longer be possible >= v0.1.5 .
|
|
||||||
|
|
||||||
If you are accessing your Home Assistant instance over `https`, you will likely
|
|
||||||
receive this error unless you have configured the card to also communicate with
|
|
||||||
Frigate via `https` (e.g. via a reverse proxy). This is because the browser is
|
|
||||||
blocking the attempt to mix access to both `https` and `http` resources.
|
|
||||||
|
|
||||||
The javascript console ([how to access](https://javascript.info/devtools)) will
|
|
||||||
show an error such as:
|
|
||||||
|
|
||||||
```
|
|
||||||
Mixed Content: The page at '<URL>' was loaded over HTTPS, but requested an
|
|
||||||
insecure resource '<URL>'. This request has been blocked; the content must be
|
|
||||||
served over HTTPS.
|
|
||||||
```
|
|
||||||
|
|
||||||
Accessing both Home Assistant and Frigate over `https` will likely resolve this
|
|
||||||
issue (e.g. through the use of a reverse proxy in front of Frigate).
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
+20
-43
@@ -41,11 +41,7 @@ import type {
|
|||||||
|
|
||||||
import { CARD_VERSION, REPO_URL } from './const.js';
|
import { CARD_VERSION, REPO_URL } from './const.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
import {
|
import { FRIGATE_BUTTON_MENU_ICON, FrigateCardMenu } from './components/menu.js';
|
||||||
FRIGATE_BUTTON_MENU_ICON,
|
|
||||||
MENU_HEIGHT,
|
|
||||||
FrigateCardMenu,
|
|
||||||
} from './components/menu.js';
|
|
||||||
import { View } from './view.js';
|
import { View } from './view.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
@@ -67,6 +63,7 @@ import './components/live.js';
|
|||||||
import './components/menu.js';
|
import './components/menu.js';
|
||||||
import './components/message.js';
|
import './components/message.js';
|
||||||
import './components/viewer.js';
|
import './components/viewer.js';
|
||||||
|
import './components/thumbnail-carousel.js';
|
||||||
import './patches/ha-camera-stream.js';
|
import './patches/ha-camera-stream.js';
|
||||||
import './patches/ha-hls-player.js';
|
import './patches/ha-hls-player.js';
|
||||||
|
|
||||||
@@ -737,15 +734,21 @@ export class FrigateCard extends LitElement {
|
|||||||
* Get the parameters to search for media related to the current view.
|
* Get the parameters to search for media related to the current view.
|
||||||
* @returns A BrowseMediaQueryParameters object.
|
* @returns A BrowseMediaQueryParameters object.
|
||||||
*/
|
*/
|
||||||
protected _getBrowseMediaQueryParameters(): BrowseMediaQueryParameters | undefined {
|
protected _getBrowseMediaQueryParameters(
|
||||||
|
mediaType?: 'clips' | 'snapshots',
|
||||||
|
): BrowseMediaQueryParameters | undefined {
|
||||||
if (
|
if (
|
||||||
!this._frigateCameraName ||
|
!this._frigateCameraName ||
|
||||||
!(this._view.isClipRelatedView() || this._view.isSnapshotRelatedView())
|
!(
|
||||||
|
this._view.isClipRelatedView() ||
|
||||||
|
this._view.isSnapshotRelatedView() ||
|
||||||
|
mediaType
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
mediaType: this._view.isClipRelatedView() ? 'clips' : 'snapshots',
|
mediaType: mediaType || (this._view.isClipRelatedView() ? 'clips' : 'snapshots'),
|
||||||
clientId: this.config.frigate.client_id,
|
clientId: this.config.frigate.client_id,
|
||||||
cameraName: this._frigateCameraName,
|
cameraName: this._frigateCameraName,
|
||||||
label: this.config.frigate.label,
|
label: this.config.frigate.label,
|
||||||
@@ -913,41 +916,13 @@ export class FrigateCard extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
const padding = this._getAspectRatioPadding();
|
const padding = this._getAspectRatioPadding();
|
||||||
const outerStyle = {},
|
const outerStyle = {};
|
||||||
innerStyle = {};
|
|
||||||
|
|
||||||
// Padding to force a particular aspect ratio.
|
// Padding to force a particular aspect ratio.
|
||||||
if (padding != null) {
|
if (padding != null) {
|
||||||
outerStyle['padding-top'] = `${padding}%`;
|
outerStyle['padding-top'] = `${padding}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special hacky treatment required when:
|
|
||||||
//
|
|
||||||
// - It's in fullscreen mode
|
|
||||||
// - It's viewing a media item
|
|
||||||
// - And the aspect ratio of the media item < aspect ratio of the window
|
|
||||||
//
|
|
||||||
// Cannot seem to scale the video by height in CSS without actually styling
|
|
||||||
// the underlying video element (which there is no access to as it's buried
|
|
||||||
// past multiple shadow roots), so instead scale the width in terms of'vh'
|
|
||||||
// (viewport height) in proportion to the aspect-ratio of the media.
|
|
||||||
if (
|
|
||||||
screenfull.isEnabled &&
|
|
||||||
screenfull.isFullscreen &&
|
|
||||||
this._view.isMediaView() &&
|
|
||||||
this._mediaShowInfo &&
|
|
||||||
this._mediaShowInfo.width / this._mediaShowInfo.height <
|
|
||||||
window.innerWidth / window.innerHeight
|
|
||||||
) {
|
|
||||||
// If the menu is outside the media (i.e. above/below) allow space for it.
|
|
||||||
const allowance = ['above', 'below'].includes(this.config.menu.mode)
|
|
||||||
? MENU_HEIGHT
|
|
||||||
: 0;
|
|
||||||
innerStyle['max-width'] = `calc(${
|
|
||||||
(100 * this._mediaShowInfo.width) / this._mediaShowInfo.height
|
|
||||||
}vh - ${allowance}px )`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentClasses = {
|
const contentClasses = {
|
||||||
'frigate-card-contents': true,
|
'frigate-card-contents': true,
|
||||||
absolute: padding != null,
|
absolute: padding != null,
|
||||||
@@ -965,7 +940,7 @@ export class FrigateCard extends LitElement {
|
|||||||
>
|
>
|
||||||
${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
|
${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
|
||||||
<div class="container outer" style="${styleMap(outerStyle)}">
|
<div class="container outer" style="${styleMap(outerStyle)}">
|
||||||
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
|
<div class="${classMap(contentClasses)}">
|
||||||
${this._frigateCameraName == undefined
|
${this._frigateCameraName == undefined
|
||||||
? until(
|
? until(
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -997,7 +972,6 @@ export class FrigateCard extends LitElement {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const mediaQueryParameters = this._getBrowseMediaQueryParameters();
|
|
||||||
|
|
||||||
const pictureElementsClasses = {
|
const pictureElementsClasses = {
|
||||||
'picture-elements': true,
|
'picture-elements': true,
|
||||||
@@ -1032,7 +1006,7 @@ export class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-gallery
|
? html` <frigate-card-gallery
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.browseMediaQueryParameters=${mediaQueryParameters}
|
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
|
||||||
class="${classMap(galleryClasses)}"
|
class="${classMap(galleryClasses)}"
|
||||||
@frigate-card:change-view=${this._changeViewHandler}
|
@frigate-card:change-view=${this._changeViewHandler}
|
||||||
@frigate-card:message=${this._messageHandler}
|
@frigate-card:message=${this._messageHandler}
|
||||||
@@ -1043,7 +1017,7 @@ export class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-viewer
|
? html` <frigate-card-viewer
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.browseMediaQueryParameters=${mediaQueryParameters}
|
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
|
||||||
.viewerConfig=${this.config.event_viewer}
|
.viewerConfig=${this.config.event_viewer}
|
||||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
class="${classMap(viewerClasses)}"
|
class="${classMap(viewerClasses)}"
|
||||||
@@ -1062,10 +1036,13 @@ export class FrigateCard extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<frigate-card-live
|
<frigate-card-live
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
|
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters(
|
||||||
|
this.config.live.controls.thumbnails.media,
|
||||||
|
)}
|
||||||
.config=${this.config}
|
.config=${this.config}
|
||||||
.frigateCameraName=${this._frigateCameraName}
|
|
||||||
.preload=${this.config.live.preload && !this._view.is('live')}
|
.preload=${this.config.live.preload && !this._view.is('live')}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
|
@frigate-card:change-view=${this._changeViewHandler}
|
||||||
@frigate-card:media-show=${this._mediaShowHandler}
|
@frigate-card:media-show=${this._mediaShowHandler}
|
||||||
@frigate-card:pause=${this._pauseHandler}
|
@frigate-card:pause=${this._pauseHandler}
|
||||||
@frigate-card:play=${this._playHandler}
|
@frigate-card:play=${this._playHandler}
|
||||||
@@ -1089,7 +1066,7 @@ export class FrigateCard extends LitElement {
|
|||||||
this._removeDynamicMenuButton(e.detail);
|
this._removeDynamicMenuButton(e.detail);
|
||||||
}}
|
}}
|
||||||
@frigate-card:condition-state-request=${(ev) => {
|
@frigate-card:condition-state-request=${(ev) => {
|
||||||
conditionStateRequestHandler(ev, this._conditionState)
|
conditionStateRequestHandler(ev, this._conditionState);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
</frigate-card-elements>
|
</frigate-card-elements>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
unsafeCSS,
|
||||||
|
PropertyValues,
|
||||||
|
} from 'lit';
|
||||||
|
import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
|
||||||
|
|
||||||
|
import { dispatchFrigateCardEvent } from '../common';
|
||||||
|
|
||||||
|
import carouselStyle from '../scss/carousel.scss';
|
||||||
|
|
||||||
|
export interface CarouselSelect {
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FrigateCardCarousel extends LitElement {
|
||||||
|
protected _options?: EmblaOptionsType;
|
||||||
|
protected _carousel?: EmblaCarouselType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to a particular slide.
|
||||||
|
* @param index Slide number.
|
||||||
|
*/
|
||||||
|
carouselScrollTo(index: number): void {
|
||||||
|
this._carousel?.scrollTo(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected slide.
|
||||||
|
* @returns The slide index or undefined if the carousel is not loaded.
|
||||||
|
*/
|
||||||
|
carouselSelected(): number | undefined {
|
||||||
|
return this._carousel?.selectedScrollSnap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated lifecycle callback for this element.
|
||||||
|
* @param changedProperties The properties that were changed in this render.
|
||||||
|
*/
|
||||||
|
updated(changedProperties: PropertyValues): void {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
if (!this._carousel) {
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
this._loadCarousel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the carousel with "slides".
|
||||||
|
*/
|
||||||
|
protected _loadCarousel(): void {
|
||||||
|
const carouselNode = this.renderRoot.querySelector(
|
||||||
|
'.embla__viewport',
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (!this._carousel && carouselNode) {
|
||||||
|
this._carousel = EmblaCarousel(carouselNode, this._options);
|
||||||
|
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
|
||||||
|
this._carousel.on('select', () => {
|
||||||
|
const selected = this.carouselSelected();
|
||||||
|
if (selected !== undefined) {
|
||||||
|
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', {
|
||||||
|
index: selected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get element styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(carouselStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,7 +131,7 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
protected _resizeHandler(): void {
|
protected _resizeHandler(): void {
|
||||||
this._columns = Math.max(
|
this._columns = Math.max(
|
||||||
DEFAULT_COLUMNS,
|
DEFAULT_COLUMNS,
|
||||||
Math.floor(this.clientWidth / MAX_THUMBNAIL_WIDTH),
|
Math.ceil(this.clientWidth / MAX_THUMBNAIL_WIDTH),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +146,8 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
// Controls the number of columns in the gallery (allows for 1px gutter).
|
// Controls the number of columns in the gallery (allows for 5px gutter).
|
||||||
width: `calc(${100 / this._columns}% - 1.2px)`,
|
width: `calc(${100 / this._columns}% - 5.25px)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return html` <ul class="mdc-image-list frigate-card-gallery">
|
return html` <ul class="mdc-image-list frigate-card-gallery">
|
||||||
|
|||||||
+49
-7
@@ -1,5 +1,6 @@
|
|||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
import type {
|
import type {
|
||||||
|
BrowseMediaQueryParameters,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
JSMPEGConfig,
|
JSMPEGConfig,
|
||||||
@@ -25,6 +26,12 @@ import { renderProgressIndicator } from '../components/message.js';
|
|||||||
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||||
|
|
||||||
import liveStyle from '../scss/live.scss';
|
import liveStyle from '../scss/live.scss';
|
||||||
|
import liveFrigateStyle from '../scss/live-frigate.scss';
|
||||||
|
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
|
||||||
|
import liveWebRTCStyle from '../scss/live-webrtc.scss';
|
||||||
|
|
||||||
|
import { View } from '../view.js';
|
||||||
|
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||||
|
|
||||||
// Number of seconds a signed URL is valid for.
|
// Number of seconds a signed URL is valid for.
|
||||||
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
@@ -41,7 +48,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
protected config?: FrigateCardConfig;
|
protected config?: FrigateCardConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected frigateCameraName?: string;
|
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
set preload(preload: boolean) {
|
set preload(preload: boolean) {
|
||||||
@@ -72,6 +79,33 @@ export class FrigateCardLive extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render thumbnails carousel.
|
||||||
|
* @returns A rendered template or void.
|
||||||
|
*/
|
||||||
|
protected renderThumbnails(): TemplateResult | void {
|
||||||
|
if (!this.config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html` <frigate-card-thumbnail-carousel
|
||||||
|
.hass=${this.hass}
|
||||||
|
.browseMediaQueryParameters=${this.browseMediaQueryParameters}
|
||||||
|
.config=${this.config?.live.controls.thumbnails}
|
||||||
|
.highlightSelected=${false}
|
||||||
|
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||||
|
const mediaType = this.browseMediaQueryParameters?.mediaType;
|
||||||
|
if (mediaType && ['snapshots', 'clips'].includes(mediaType)) {
|
||||||
|
new View({
|
||||||
|
view: mediaType === 'clips' ? 'clip-specific' : 'snapshot-specific',
|
||||||
|
target: ev.detail.target,
|
||||||
|
childIndex: ev.detail.childIndex,
|
||||||
|
}).dispatchChangeEvent(this);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</frigate-card-thumbnail-carousel>`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -81,7 +115,11 @@ export class FrigateCardLive extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` ${this.config.live.provider == 'frigate'
|
return html`
|
||||||
|
${this.config.live.controls.thumbnails.mode === 'above'
|
||||||
|
? this.renderThumbnails()
|
||||||
|
: ''}
|
||||||
|
${this.config.live.provider == 'frigate'
|
||||||
? html` <frigate-card-live-frigate
|
? html` <frigate-card-live-frigate
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraEntity=${this.config.camera_entity}
|
.cameraEntity=${this.config.camera_entity}
|
||||||
@@ -97,12 +135,16 @@ export class FrigateCardLive extends LitElement {
|
|||||||
</frigate-card-live-webrtc>`
|
</frigate-card-live-webrtc>`
|
||||||
: html` <frigate-card-live-jsmpeg
|
: html` <frigate-card-live-jsmpeg
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraName=${this.frigateCameraName}
|
.cameraName=${this.browseMediaQueryParameters?.cameraName}
|
||||||
.clientId=${this.config.frigate.client_id}
|
.clientId=${this.config.frigate.client_id}
|
||||||
.jsmpegConfig=${this.config.live.jsmpeg}
|
.jsmpegConfig=${this.config.live.jsmpeg}
|
||||||
@frigate-card:media-show=${this._mediaShowHandler}
|
@frigate-card:media-show=${this._mediaShowHandler}
|
||||||
>
|
>
|
||||||
</frigate-card-live-jsmpeg>`}`;
|
</frigate-card-live-jsmpeg>`}
|
||||||
|
${this.config.live.controls.thumbnails.mode === 'below'
|
||||||
|
? this.renderThumbnails()
|
||||||
|
: ''}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +192,7 @@ export class FrigateCardLiveFrigate extends LitElement {
|
|||||||
* Get styles.
|
* Get styles.
|
||||||
*/
|
*/
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(liveStyle);
|
return unsafeCSS(liveFrigateStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +279,7 @@ export class FrigateCardLiveWebRTC extends LitElement {
|
|||||||
* Get styles.
|
* Get styles.
|
||||||
*/
|
*/
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(liveStyle);
|
return unsafeCSS(liveWebRTCStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +457,6 @@ export class FrigateCardLiveJSMPEG extends LitElement {
|
|||||||
* Get styles.
|
* Get styles.
|
||||||
*/
|
*/
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(liveStyle);
|
return unsafeCSS(liveJSMPEGStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
import menuStyle from '../scss/menu.scss';
|
import menuStyle from '../scss/menu.scss';
|
||||||
import { ConditionState, evaluateCondition } from '../card-condition.js';
|
import { ConditionState, evaluateCondition } from '../card-condition.js';
|
||||||
|
|
||||||
export const MENU_HEIGHT = 46;
|
|
||||||
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
|
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +45,7 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
|
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public _menuConfig?: MenuConfig;
|
protected _menuConfig?: MenuConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected expand = false;
|
protected expand = false;
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
|
import { CSSResultGroup, TemplateResult, html, unsafeCSS, LitElement } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { until } from 'lit/directives/until';
|
||||||
|
|
||||||
|
import type { BrowseMediaQueryParameters, BrowseMediaSource, ExtendedHomeAssistant, ThumbnailsControlConfig } from '../types.js';
|
||||||
|
import { FrigateCardCarousel } from './carousel.js';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
|
import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js';
|
||||||
|
import { renderProgressIndicator } from './message.js';
|
||||||
|
|
||||||
|
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||||
|
|
||||||
|
export interface ThumbnailCarouselTap {
|
||||||
|
slideIndex: number;
|
||||||
|
target: BrowseMediaSource;
|
||||||
|
childIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-thumbnail-carousel')
|
||||||
|
export class FrigateCardThumbnailCarousel extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected config?: ThumbnailsControlConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected highlightSelected = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html`${until(this._render(), renderProgressIndicator())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asyncronously render the element.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected async _render(): Promise<TemplateResult | void> {
|
||||||
|
if (!this.hass || !this.browseMediaQueryParameters) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent: BrowseMediaSource | null = null;
|
||||||
|
try {
|
||||||
|
parent = await BrowseMediaUtil.browseMediaQuery(
|
||||||
|
this.hass,
|
||||||
|
this.browseMediaQueryParameters,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return dispatchErrorMessageEvent(this, (e as Error).message);
|
||||||
|
}
|
||||||
|
return html` <frigate-card-thumbnail-carousel-core
|
||||||
|
.target=${parent}
|
||||||
|
.config=${this.config}
|
||||||
|
.highlightSelected=${this.highlightSelected}
|
||||||
|
>
|
||||||
|
</frigate-card-thumbnail-carousel-core>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-thumbnail-carousel-core')
|
||||||
|
export class FrigateCardThumbnailCarouselCore extends FrigateCardCarousel {
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected target?: BrowseMediaSource;
|
||||||
|
|
||||||
|
protected _tapSelected?;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
set config(config: ThumbnailsControlConfig | undefined) {
|
||||||
|
if (config) {
|
||||||
|
if (config && (config.size !== undefined && config.size !== null)) {
|
||||||
|
this.style.setProperty('--frigate-card-carousel-thumbnail-size', config.size);
|
||||||
|
}
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected _config?: ThumbnailsControlConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
set highlightSelected(value: boolean) {
|
||||||
|
this.style.setProperty('--frigate-card-carousel-thumbnail-opacity', value ? '0.6' : '1.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._options = {
|
||||||
|
containScroll: 'keepSnaps',
|
||||||
|
dragFree: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll to a particular slide.
|
||||||
|
* @param index Slide number.
|
||||||
|
*/
|
||||||
|
carouselScrollTo(index: number): void {
|
||||||
|
if (!this._carousel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._tapSelected !== undefined) {
|
||||||
|
this._carousel.slideNodes()[this._tapSelected].classList.remove('slide-selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
super.carouselScrollTo(index);
|
||||||
|
|
||||||
|
this._carousel.slideNodes()[index].classList.add('slide-selected');
|
||||||
|
this._tapSelected = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get slides to include in the render.
|
||||||
|
* @returns The slides to include in the render.
|
||||||
|
*/
|
||||||
|
protected _getSlides(): TemplateResult[] {
|
||||||
|
if (!this.target || !this.target.children || !this.target.children.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const slides: TemplateResult[] = [];
|
||||||
|
for (let i = 0; i < this.target.children.length; ++i) {
|
||||||
|
const thumbnail = this._renderThumbnail(
|
||||||
|
this.target,
|
||||||
|
i,
|
||||||
|
slides.length);
|
||||||
|
if (thumbnail) {
|
||||||
|
slides.push(thumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slides;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a given thumbnail.
|
||||||
|
* @param mediaToRender The media item to render.
|
||||||
|
* @returns A template or void if the item could not be rendered.
|
||||||
|
*/
|
||||||
|
protected _renderThumbnail(
|
||||||
|
parent: BrowseMediaSource,
|
||||||
|
childIndex: number,
|
||||||
|
slideIndex: number,
|
||||||
|
): TemplateResult | void {
|
||||||
|
if (!parent.children || !parent.children.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaToRender = parent.children[childIndex];
|
||||||
|
if (!BrowseMediaUtil.isTrueMedia(mediaToRender) || !mediaToRender.thumbnail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<div
|
||||||
|
class="embla__slide"
|
||||||
|
.actionHandler=${actionHandler({
|
||||||
|
hasHold: false,
|
||||||
|
hasDoubleClick: false,
|
||||||
|
})}
|
||||||
|
@action=${() => {
|
||||||
|
if (this._carousel && this._carousel.clickAllowed()) {
|
||||||
|
dispatchFrigateCardEvent<ThumbnailCarouselTap>(this, 'carousel:tap', {
|
||||||
|
slideIndex: slideIndex,
|
||||||
|
target: parent,
|
||||||
|
childIndex: childIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src="${mediaToRender.thumbnail}" title="${mediaToRender.title}" />
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the element.
|
||||||
|
* @returns A template to display to the user.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
const slides = this._getSlides();
|
||||||
|
if (!slides || !this._config || this._config.mode == 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html` <div class="embla">
|
||||||
|
<div class="embla__viewport">
|
||||||
|
<div class="embla__container">${slides}</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get element styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [super.styles, unsafeCSS(thumbnailCarouselStyle)];
|
||||||
|
}
|
||||||
|
}
|
||||||
+120
-59
@@ -1,14 +1,8 @@
|
|||||||
import {
|
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
CSSResultGroup,
|
|
||||||
LitElement,
|
|
||||||
TemplateResult,
|
|
||||||
html,
|
|
||||||
unsafeCSS,
|
|
||||||
PropertyValues,
|
|
||||||
} from 'lit';
|
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel';
|
import { EmblaCarouselType } from 'embla-carousel';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
@@ -21,8 +15,11 @@ import type {
|
|||||||
MediaShowInfo,
|
MediaShowInfo,
|
||||||
ViewerConfig,
|
ViewerConfig,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
import { FrigateCardCarousel } from './carousel.js';
|
||||||
|
import { FrigateCardThumbnailCarouselCore, ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||||
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
|
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
import {
|
import {
|
||||||
createMediaShowInfo,
|
createMediaShowInfo,
|
||||||
dispatchErrorMessageEvent,
|
dispatchErrorMessageEvent,
|
||||||
@@ -38,7 +35,8 @@ import { renderProgressIndicator } from '../components/message.js';
|
|||||||
import './next-prev-control.js';
|
import './next-prev-control.js';
|
||||||
|
|
||||||
import viewerStyle from '../scss/viewer.scss';
|
import viewerStyle from '../scss/viewer.scss';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
import viewerCoreStyle from '../scss/viewer-core.scss';
|
||||||
|
import mediaCarouselStyle from '../scss/media-carousel.scss';
|
||||||
|
|
||||||
const getEmptyImageSrc = (width: number, height: number) =>
|
const getEmptyImageSrc = (width: number, height: number) =>
|
||||||
`data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
|
`data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
|
||||||
@@ -166,9 +164,80 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
// Media carousel object.
|
protected _mediaCarouselRef: Ref<FrigateCardMediaCarousel> = createRef();
|
||||||
protected _carousel?: EmblaCarouselType;
|
protected _thumbnailCarouselRef: Ref<FrigateCardThumbnailCarouselCore> = createRef();
|
||||||
protected _loadedCarousel = false;
|
|
||||||
|
protected _syncThumbnailCarousel(): void {
|
||||||
|
const mediaSelected = this._mediaCarouselRef.value?.carouselSelected();
|
||||||
|
if (mediaSelected !== undefined) {
|
||||||
|
this._thumbnailCarouselRef.value?.carouselScrollTo(mediaSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _renderThumbnails(): TemplateResult {
|
||||||
|
if (!this.view || !this.viewerConfig) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html` <frigate-card-thumbnail-carousel-core
|
||||||
|
${ref(this._thumbnailCarouselRef)}
|
||||||
|
.target=${this.view.target}
|
||||||
|
.config=${this.viewerConfig.controls.thumbnails}
|
||||||
|
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||||
|
this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.slideIndex);
|
||||||
|
}}
|
||||||
|
@frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-thumbnail-carousel-core>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.view || !this.viewerConfig) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html` ${this.viewerConfig &&
|
||||||
|
this.viewerConfig.controls.thumbnails.mode === 'above'
|
||||||
|
? this._renderThumbnails()
|
||||||
|
: ''}
|
||||||
|
<frigate-card-media-carousel
|
||||||
|
${ref(this._mediaCarouselRef)}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.view=${this.view}
|
||||||
|
.viewerConfig=${this.viewerConfig}
|
||||||
|
.browseMediaQueryParameters=${this.browseMediaQueryParameters}
|
||||||
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
|
@frigate-card:carousel:select=${this._syncThumbnailCarousel.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-media-carousel>
|
||||||
|
${this.viewerConfig && this.viewerConfig.controls.thumbnails.mode === 'below'
|
||||||
|
? this._renderThumbnails()
|
||||||
|
: ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get element styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(viewerCoreStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-media-carousel')
|
||||||
|
export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected view?: View;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected viewerConfig?: ViewerConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
// Mapping of slide # to BrowseMediaSource child #.
|
// Mapping of slide # to BrowseMediaSource child #.
|
||||||
// (Folders are not media items that can be rendered).
|
// (Folders are not media items that can be rendered).
|
||||||
@@ -180,30 +249,13 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
// Whether or not a given slide has been successfully lazily loaded.
|
// Whether or not a given slide has been successfully lazily loaded.
|
||||||
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
|
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
|
||||||
|
|
||||||
/**
|
|
||||||
* The updated lifecycle callback for this element.
|
|
||||||
* @param changedProperties The properties that were changed in this render.
|
|
||||||
*/
|
|
||||||
updated(changedProperties: PropertyValues): void {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
|
|
||||||
if (!this._loadedCarousel) {
|
|
||||||
this.updateComplete.then(() => {
|
|
||||||
this._loadCarousel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the carousel with "slides" (clips or snapshots).
|
* Load the carousel with "slides" (clips or snapshots).
|
||||||
*/
|
*/
|
||||||
protected _loadCarousel(): void {
|
protected _loadCarousel(): void {
|
||||||
const carouselNode = this.renderRoot.querySelector(
|
if (this._carousel || !this.viewerConfig) {
|
||||||
'.embla__viewport',
|
return;
|
||||||
) as HTMLElement;
|
}
|
||||||
|
|
||||||
if (carouselNode && this.viewerConfig) {
|
|
||||||
this._loadedCarousel = true;
|
|
||||||
|
|
||||||
// Start the carousel on the selected child number.
|
// Start the carousel on the selected child number.
|
||||||
const startIndex = Number(
|
const startIndex = Number(
|
||||||
@@ -212,21 +264,27 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._carousel = EmblaCarousel(carouselNode, {
|
this._options = {
|
||||||
startIndex: isNaN(startIndex) ? undefined : startIndex,
|
startIndex: isNaN(startIndex) ? undefined : startIndex,
|
||||||
draggable: this.viewerConfig.draggable,
|
draggable: this.viewerConfig.draggable,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
super._loadCarousel();
|
||||||
|
|
||||||
|
// Necessary because typescript local type narrowing is not paying attention
|
||||||
|
// to the side-effect of the call to super._loadCarousel().
|
||||||
|
const carousel = this._carousel as EmblaCarouselType | undefined;
|
||||||
|
|
||||||
// Update views and dispatch media-show events based on slide selections.
|
// Update views and dispatch media-show events based on slide selections.
|
||||||
this._carousel.on('select', this._selectSlideSetViewHandler.bind(this));
|
carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
|
||||||
this._carousel.on('select', this._selectSlideMediaShowHandler.bind(this));
|
carousel?.on('select', this._selectSlideMediaShowHandler.bind(this));
|
||||||
|
|
||||||
// Lazily load media that is displayed. These handlers are registered
|
// Lazily load media that is displayed. These handlers are registered
|
||||||
// regardless of the value of this.lazyLoad to allow that value to change
|
// regardless of the value of this.lazyLoad to allow that value to change
|
||||||
// after the carousel has been initialized.
|
// after the carousel has been initialized.
|
||||||
this._carousel.on('init', this._lazyLoadMediaHandler.bind(this));
|
carousel?.on('init', this._lazyLoadMediaHandler.bind(this));
|
||||||
this._carousel.on('select', this._lazyLoadMediaHandler.bind(this));
|
carousel?.on('select', this._lazyLoadMediaHandler.bind(this));
|
||||||
this._carousel.on('resize', this._lazyLoadMediaHandler.bind(this));
|
carousel?.on('resize', this._lazyLoadMediaHandler.bind(this));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -451,35 +509,45 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the element.
|
* Get slides to include in the render.
|
||||||
* @returns A template to display to the user.
|
* @returns The slides to include in the render.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected _getSlides(): TemplateResult[] {
|
||||||
if (
|
if (
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.view.target ||
|
!this.view.target ||
|
||||||
!this.view.target.children ||
|
!this.view.target.children ||
|
||||||
!this.view.target.children.length ||
|
!this.view.target.children.length
|
||||||
this.view.childIndex === undefined ||
|
|
||||||
!this.resolvedMediaCache
|
|
||||||
) {
|
) {
|
||||||
return html``;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const slides: TemplateResult[] = [];
|
|
||||||
this._slideToChild = {};
|
this._slideToChild = {};
|
||||||
|
const slides: TemplateResult[] = [];
|
||||||
for (let i = 0; i < this.view.target.children?.length; ++i) {
|
for (let i = 0; i < this.view.target.children?.length; ++i) {
|
||||||
const slide = this._renderMediaItem(this.view.target.children[i], slides.length);
|
const slide = this._renderMediaItem(this.view.target.children[i], slides.length);
|
||||||
|
|
||||||
if (slide) {
|
if (slide) {
|
||||||
this._slideToChild[slides.length] = i;
|
this._slideToChild[slides.length] = i;
|
||||||
slides.push(slide);
|
slides.push(slide);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return slides;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the element.
|
||||||
|
* @returns A template to display to the user.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
const slides = this._getSlides();
|
||||||
|
if (!slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const neighbors = this._getMediaNeighbors();
|
const neighbors = this._getMediaNeighbors();
|
||||||
|
|
||||||
return html`<div class="container">
|
return html`<div class="embla">
|
||||||
${neighbors && neighbors.previous
|
${neighbors && neighbors.previous
|
||||||
? html`<frigate-card-next-previous-control
|
? html`<frigate-card-next-previous-control
|
||||||
.direction=${'previous'}
|
.direction=${'previous'}
|
||||||
@@ -495,11 +563,9 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
}}
|
}}
|
||||||
></frigate-card-next-previous-control>`
|
></frigate-card-next-previous-control>`
|
||||||
: ``}
|
: ``}
|
||||||
<div class="embla">
|
|
||||||
<div class="embla__viewport">
|
<div class="embla__viewport">
|
||||||
<div class="embla__container">${slides}</div>
|
<div class="embla__container">${slides}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
${neighbors && neighbors.next
|
${neighbors && neighbors.next
|
||||||
? html`<frigate-card-next-previous-control
|
? html`<frigate-card-next-previous-control
|
||||||
.direction=${'next'}
|
.direction=${'next'}
|
||||||
@@ -595,11 +661,6 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a given media item.
|
|
||||||
* @param mediaToRender The media item to render.
|
|
||||||
* @returns A template or void if the item could not be rendered.
|
|
||||||
*/
|
|
||||||
protected _renderMediaItem(
|
protected _renderMediaItem(
|
||||||
mediaToRender: BrowseMediaSource,
|
mediaToRender: BrowseMediaSource,
|
||||||
slideIndex: number,
|
slideIndex: number,
|
||||||
@@ -702,6 +763,6 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
* Get element styles.
|
* Get element styles.
|
||||||
*/
|
*/
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(viewerStyle);
|
return [super.styles, unsafeCSS(mediaCarouselStyle)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,23 @@ export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE =
|
|||||||
'event_viewer.controls.next_previous.style';
|
'event_viewer.controls.next_previous.style';
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
|
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
|
||||||
'event_viewer.controls.next_previous.size';
|
'event_viewer.controls.next_previous.size';
|
||||||
|
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE =
|
||||||
|
'event_viewer.controls.thumbnails.mode';
|
||||||
|
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE =
|
||||||
|
'event_viewer.controls.thumbnails.size';
|
||||||
|
|
||||||
export const CONF_LIVE_PRELOAD = 'live.preload';
|
export const CONF_LIVE_PRELOAD = 'live.preload';
|
||||||
export const CONF_LIVE_PROVIDER = 'live.provider';
|
export const CONF_LIVE_PROVIDER = 'live.provider';
|
||||||
export const CONF_LIVE_WEBRTC_ENTITY = 'live.webrtc.entity';
|
export const CONF_LIVE_WEBRTC_ENTITY = 'live.webrtc.entity';
|
||||||
export const CONF_LIVE_WEBRTC_URL = 'live.webrtc.url';
|
export const CONF_LIVE_WEBRTC_URL = 'live.webrtc.url';
|
||||||
|
|
||||||
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE =
|
||||||
|
'live.controls.thumbnails.mode';
|
||||||
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE =
|
||||||
|
'live.controls.thumbnails.size';
|
||||||
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA =
|
||||||
|
'live.controls.thumbnails.media';
|
||||||
|
|
||||||
export const CONF_IMAGE_SRC = 'image.src';
|
export const CONF_IMAGE_SRC = 'image.src';
|
||||||
|
|
||||||
export const CONF_MENU_BUTTONS_FRIGATE = 'menu.buttons.frigate';
|
export const CONF_MENU_BUTTONS_FRIGATE = 'menu.buttons.frigate';
|
||||||
|
|||||||
+122
@@ -23,6 +23,8 @@ import {
|
|||||||
CONF_EVENT_VIEWER_AUTOPLAY_CLIP,
|
CONF_EVENT_VIEWER_AUTOPLAY_CLIP,
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
||||||
CONF_EVENT_VIEWER_DRAGGABLE,
|
CONF_EVENT_VIEWER_DRAGGABLE,
|
||||||
CONF_EVENT_VIEWER_LAZY_LOAD,
|
CONF_EVENT_VIEWER_LAZY_LOAD,
|
||||||
CONF_FRIGATE_CAMERA_NAME,
|
CONF_FRIGATE_CAMERA_NAME,
|
||||||
@@ -31,6 +33,9 @@ import {
|
|||||||
CONF_FRIGATE_URL,
|
CONF_FRIGATE_URL,
|
||||||
CONF_FRIGATE_ZONE,
|
CONF_FRIGATE_ZONE,
|
||||||
CONF_IMAGE_SRC,
|
CONF_IMAGE_SRC,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
CONF_LIVE_PRELOAD,
|
CONF_LIVE_PRELOAD,
|
||||||
CONF_LIVE_PROVIDER,
|
CONF_LIVE_PROVIDER,
|
||||||
CONF_LIVE_WEBRTC_ENTITY,
|
CONF_LIVE_WEBRTC_ENTITY,
|
||||||
@@ -223,6 +228,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
unconstrained: localize('config.dimensions.aspect_ratio_modes.unconstrained'),
|
unconstrained: localize('config.dimensions.aspect_ratio_modes.unconstrained'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const thumbnailModes = {
|
||||||
|
'': '',
|
||||||
|
none: localize('config.event_viewer.controls.thumbnails.modes.none'),
|
||||||
|
above: localize('config.event_viewer.controls.thumbnails.modes.above'),
|
||||||
|
below: localize('config.event_viewer.controls.thumbnails.modes.below'),
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailMedias = {
|
||||||
|
'': '',
|
||||||
|
clips: localize('config.live.controls.thumbnails.medias.clips'),
|
||||||
|
snapshots: localize('config.live.controls.thumbnails.medias.snapshots'),
|
||||||
|
}
|
||||||
|
|
||||||
const defaults = frigateCardConfigDefaults;
|
const defaults = frigateCardConfigDefaults;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -588,6 +606,72 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
.configValue=${CONF_LIVE_WEBRTC_URL}
|
.configValue=${CONF_LIVE_WEBRTC_URL}
|
||||||
@value-changed=${this._valueChangedHandler}
|
@value-changed=${this._valueChangedHandler}
|
||||||
></paper-input>
|
></paper-input>
|
||||||
|
<paper-dropdown-menu
|
||||||
|
.label=${localize(
|
||||||
|
`config.${CONF_LIVE_CONTROLS_THUMBNAILS_MODE}`,
|
||||||
|
)}
|
||||||
|
@value-changed=${this._valueChangedHandler}
|
||||||
|
.configValue=${CONF_LIVE_CONTROLS_THUMBNAILS_MODE}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${Object.keys(thumbnailModes).indexOf(
|
||||||
|
String(
|
||||||
|
getConfigValue(
|
||||||
|
this._config,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${Object.keys(thumbnailModes).map((key) => {
|
||||||
|
return html`
|
||||||
|
<paper-item .label="${key}">
|
||||||
|
${thumbnailModes[key]}
|
||||||
|
</paper-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
<paper-dropdown-menu
|
||||||
|
.label=${localize(
|
||||||
|
`config.${CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA}`,
|
||||||
|
)}
|
||||||
|
@value-changed=${this._valueChangedHandler}
|
||||||
|
.configValue=${CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${Object.keys(thumbnailMedias).indexOf(
|
||||||
|
String(
|
||||||
|
getConfigValue(
|
||||||
|
this._config,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${Object.keys(thumbnailMedias).map((key) => {
|
||||||
|
return html`
|
||||||
|
<paper-item .label="${key}">
|
||||||
|
${thumbnailMedias[key]}
|
||||||
|
</paper-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
<paper-input
|
||||||
|
label=${localize(`config.${CONF_LIVE_CONTROLS_THUMBNAILS_SIZE}`)}
|
||||||
|
.value=${getConfigValue(
|
||||||
|
this._config,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
'',
|
||||||
|
)}
|
||||||
|
.configValue=${CONF_LIVE_CONTROLS_THUMBNAILS_SIZE}
|
||||||
|
@value-changed=${this._valueChangedHandler}
|
||||||
|
></paper-input>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
@@ -678,6 +762,44 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
.configValue=${CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE}
|
.configValue=${CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE}
|
||||||
@value-changed=${this._valueChangedHandler}
|
@value-changed=${this._valueChangedHandler}
|
||||||
></paper-input>
|
></paper-input>
|
||||||
|
<paper-dropdown-menu
|
||||||
|
.label=${localize(
|
||||||
|
`config.${CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE}`,
|
||||||
|
)}
|
||||||
|
@value-changed=${this._valueChangedHandler}
|
||||||
|
.configValue=${CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${Object.keys(thumbnailModes).indexOf(
|
||||||
|
String(
|
||||||
|
getConfigValue(
|
||||||
|
this._config,
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${Object.keys(thumbnailModes).map((key) => {
|
||||||
|
return html`
|
||||||
|
<paper-item .label="${key}">
|
||||||
|
${thumbnailModes[key]}
|
||||||
|
</paper-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
|
<paper-input
|
||||||
|
label=${localize(`config.${CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE}`)}
|
||||||
|
.value=${getConfigValue(
|
||||||
|
this._config,
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
'',
|
||||||
|
)}
|
||||||
|
.configValue=${CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE}
|
||||||
|
@value-changed=${this._valueChangedHandler}
|
||||||
|
></paper-input>
|
||||||
</div>`
|
</div>`
|
||||||
: ''}
|
: ''}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -42,6 +42,15 @@
|
|||||||
"chevrons": "Chevrons",
|
"chevrons": "Chevrons",
|
||||||
"none": "None"
|
"none": "None"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"mode": "Event Viewer thumbnails mode",
|
||||||
|
"size": "Event Viewer thumbnails size (e.g. '100px')",
|
||||||
|
"modes": {
|
||||||
|
"below": "Thumbnails below the media",
|
||||||
|
"above": "Thumbnails above the media",
|
||||||
|
"none": "No thumbnails"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -56,6 +65,17 @@
|
|||||||
"webrtc": {
|
"webrtc": {
|
||||||
"entity": "WebRTC Camera Entity (Not a Frigate camera)",
|
"entity": "WebRTC Camera Entity (Not a Frigate camera)",
|
||||||
"url": "WebRTC Camera URL"
|
"url": "WebRTC Camera URL"
|
||||||
|
},
|
||||||
|
"controls": {
|
||||||
|
"thumbnails": {
|
||||||
|
"mode": "Live thumbnails mode",
|
||||||
|
"size": "Live thumbnails size (e.g. '100px')",
|
||||||
|
"media": "Whether to show thumbnails of clips or snapshots",
|
||||||
|
"medias": {
|
||||||
|
"clips": "Clip thumbnails",
|
||||||
|
"snapshots": "Snapshot thumbnails"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
// available as compilation time.
|
// available as compilation time.
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
import { TemplateResult, html } from 'lit';
|
import { TemplateResult, css, html } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { dispatchMediaShowEvent } from '../common.js';
|
import { dispatchMediaShowEvent } from '../common.js';
|
||||||
|
|
||||||
@@ -75,5 +75,16 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
: ''}
|
: ''}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}`
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
// available as compilation time.
|
// available as compilation time.
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
import { TemplateResult, html } from 'lit';
|
import { TemplateResult, css, html } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
dispatchMediaShowEvent,
|
dispatchMediaShowEvent,
|
||||||
dispatchPauseEvent,
|
dispatchPauseEvent,
|
||||||
@@ -44,5 +45,22 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
></video>
|
></video>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
video {
|
||||||
|
object-fit: contain;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-8
@@ -47,7 +47,8 @@
|
|||||||
/* A relative div to place absolute picture elements onto */
|
/* A relative div to place absolute picture elements onto */
|
||||||
.picture-elements {
|
.picture-elements {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: inherit;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enforce picture elements to only be the size of the card/fullscreen (and not
|
/* Enforce picture elements to only be the size of the card/fullscreen (and not
|
||||||
@@ -68,13 +69,6 @@ ha-card {
|
|||||||
background-color: var(--secondary-background-color, black);
|
background-color: var(--secondary-background-color, black);
|
||||||
}
|
}
|
||||||
|
|
||||||
frigate-card-gallery, frigate-card-viewer, frigate-card-live, frigate-card-message, frigate-card-error-message {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
frigate-card-gallery {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
frigate-card-gallery.hidden,frigate-card-viewer.hidden,frigate-card-live.hidden {
|
frigate-card-gallery.hidden,frigate-card-viewer.hidden,frigate-card-live.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embla {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embla__container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embla__viewport {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.embla__viewport.is-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.embla__viewport.is-dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embla__slide {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 5px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.embla__slide img,video {
|
||||||
|
// Letterbox media. <frigate-card-ha-hls-player> has similar added directly in
|
||||||
|
// its element.
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
+12
-3
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
-ms-overflow-style: none; /* Hide scrollbar: IE and Edge */
|
-ms-overflow-style: none; /* Hide scrollbar: IE and Edge */
|
||||||
scrollbar-width: none; /* Hide scrollbar: Firefox */
|
scrollbar-width: none; /* Hide scrollbar: Firefox */
|
||||||
@@ -16,20 +18,27 @@
|
|||||||
.frigate-card-gallery {
|
.frigate-card-gallery {
|
||||||
// Note: In fullscreen, number of columns is overwritten in Javascript based
|
// Note: In fullscreen, number of columns is overwritten in Javascript based
|
||||||
// on dimensions.
|
// on dimensions.
|
||||||
@include image-list.standard-columns(5, 1px);
|
@include image-list.standard-columns(4, 5px);
|
||||||
@include image-list.shape-radius(5px);
|
@include image-list.shape-radius(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.frigate-card-gallery .mdc-image-list__image {
|
||||||
|
transition: transform 0.2s linear;
|
||||||
|
}
|
||||||
|
.frigate-card-gallery .mdc-image-list__image:hover {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
ha-card.frigate-card-gallery-folder {
|
ha-card.frigate-card-gallery-folder {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-style: 2px solid;
|
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
color: var(--secondary-text-color, white);
|
color: var(--secondary-text-color, white);
|
||||||
border-color: var(--secondary-text-color, black);
|
border: 2px ridge var(--secondary-text-color, black);
|
||||||
|
border-radius: 5px;
|
||||||
background-color: var(--primary-background-color, black);
|
background-color: var(--primary-background-color, black);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
--video-max-height: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Don't drop shadow or have radius for nested webrtc card */
|
||||||
|
webrtc-camera ha-card {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: var(--secondary-background-color, black);
|
||||||
|
}
|
||||||
+11
-10
@@ -1,15 +1,16 @@
|
|||||||
:host {
|
:host {
|
||||||
--video-max-height: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Don't drop shadow or have radius for nested webrtc card */
|
frigate-card-live {
|
||||||
webrtc-camera ha-card {
|
flex: 1;
|
||||||
box-shadow: none;
|
min-height: 0;
|
||||||
border-radius: 0px;
|
}
|
||||||
background-color: var(--secondary-background-color, black);
|
|
||||||
|
frigate-card-thumbnail-carousel {
|
||||||
|
flex: 0 0 var(--frigate-card-carousel-thumbnail-size);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
:host {
|
||||||
|
--video-max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embla__slide {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate-card-ha-hls-player {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
:host {
|
||||||
|
--frigate-card-carousel-thumbnail-size: 100px;
|
||||||
|
--frigate-card-carousel-thumbnail-opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embla__slide {
|
||||||
|
flex: 0 0 var(--frigate-card-carousel-thumbnail-size);
|
||||||
|
opacity: var(--frigate-card-carousel-thumbnail-opacity);
|
||||||
|
transition: opacity 0.6s ease, transform 0.2s linear;
|
||||||
|
}
|
||||||
|
.embla__slide.slide-selected {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
.embla__slide:hover {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
.embla__slide img {
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
// Not 'contain' as some thumbnails may vary in aspect-ratio slightly and
|
||||||
|
// should be clipped to fill the thumbnail div whilst maintaining
|
||||||
|
// aspect-ratio.
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate-card-media-carousel {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate-card-thumbnail-carousel-core {
|
||||||
|
flex: 0 0 var(--frigate-card-carousel-thumbnail-size);
|
||||||
|
}
|
||||||
+1
-50
@@ -1,54 +1,5 @@
|
|||||||
:host {
|
:host {
|
||||||
--video-max-height: none;
|
height: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
ha-hls-player {
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
}
|
|
||||||
|
|
||||||
img,video {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.container {
|
|
||||||
// Keep the controls positioned relative to the video.
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embla {
|
|
||||||
position: relative;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embla__viewport {
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embla__container {
|
|
||||||
display: flex;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embla__viewport.is-draggable {
|
|
||||||
cursor: move;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embla__viewport.is-dragging {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embla__slide {
|
|
||||||
position: relative;
|
|
||||||
min-width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin-right: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
@@ -355,12 +355,29 @@ const imageConfigSchema = z
|
|||||||
.optional();
|
.optional();
|
||||||
export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
|
export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thumbnail controls configuration section.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This type is not inferred from a schema since different (compatible) schemas
|
||||||
|
// have different defaults.
|
||||||
|
export type ThumbnailsControlConfig = {
|
||||||
|
mode: 'above' | 'below' | 'none';
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live view configuration section.
|
* Live view configuration section.
|
||||||
*/
|
*/
|
||||||
const liveConfigDefault = {
|
const liveConfigDefault = {
|
||||||
provider: 'frigate' as const,
|
provider: 'frigate' as const,
|
||||||
preload: false,
|
preload: false,
|
||||||
|
controls: {
|
||||||
|
thumbnails: {
|
||||||
|
mode: 'none' as const,
|
||||||
|
media: 'clips' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const webrtcConfigSchema = z
|
const webrtcConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -400,6 +417,19 @@ const liveConfigSchema = z
|
|||||||
preload: z.boolean().default(liveConfigDefault.preload),
|
preload: z.boolean().default(liveConfigDefault.preload),
|
||||||
webrtc: webrtcConfigSchema,
|
webrtc: webrtcConfigSchema,
|
||||||
jsmpeg: jsmpegConfigSchema,
|
jsmpeg: jsmpegConfigSchema,
|
||||||
|
controls: z
|
||||||
|
.object({
|
||||||
|
thumbnails: z
|
||||||
|
.object({
|
||||||
|
media: z
|
||||||
|
.enum(['clips', 'snapshots'])
|
||||||
|
.default(liveConfigDefault.controls.thumbnails.media),
|
||||||
|
mode: z.enum(['none', 'above', 'below']).default(liveConfigDefault.controls.thumbnails.mode),
|
||||||
|
size: z.string().optional(),
|
||||||
|
})
|
||||||
|
.default(liveConfigDefault.controls.thumbnails),
|
||||||
|
})
|
||||||
|
.default(liveConfigDefault.controls),
|
||||||
})
|
})
|
||||||
.merge(actionsSchema)
|
.merge(actionsSchema)
|
||||||
.default(liveConfigDefault);
|
.default(liveConfigDefault);
|
||||||
@@ -454,6 +484,9 @@ const viewerConfigDefault = {
|
|||||||
size: '48px',
|
size: '48px',
|
||||||
style: 'thumbnails' as const,
|
style: 'thumbnails' as const,
|
||||||
},
|
},
|
||||||
|
thumbnails: {
|
||||||
|
mode: 'below' as const,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const nextPreviousControlConfigSchema = z
|
const nextPreviousControlConfigSchema = z
|
||||||
@@ -474,6 +507,10 @@ const viewerConfigSchema = z
|
|||||||
controls: z
|
controls: z
|
||||||
.object({
|
.object({
|
||||||
next_previous: nextPreviousControlConfigSchema,
|
next_previous: nextPreviousControlConfigSchema,
|
||||||
|
thumbnails: z.object({
|
||||||
|
mode: z.enum(['none', 'above', 'below']).default(viewerConfigDefault.controls.thumbnails.mode),
|
||||||
|
size: z.string().optional(),
|
||||||
|
}).default(viewerConfigDefault.controls.thumbnails),
|
||||||
})
|
})
|
||||||
.default(viewerConfigDefault.controls),
|
.default(viewerConfigDefault.controls),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user