Merge pull request #619 from dermotduffy/recordings
Add recording support
This commit is contained in:
@@ -359,17 +359,17 @@ live:
|
|||||||
| `mode` | `popup-bottom-right` | :white_check_mark: | How to display the live camera title. Acceptable values: `none`, `popup-top-left`, `popup-top-right`, `popup-bottom-left`, `popup-bottom-right` . |
|
| `mode` | `popup-bottom-right` | :white_check_mark: | How to display the live camera title. Acceptable values: `none`, `popup-top-left`, `popup-top-right`, `popup-bottom-left`, `popup-bottom-right` . |
|
||||||
| `duration_seconds` | `2` | :white_check_mark: | The number of seconds to display the title popup. `0` implies forever.|
|
| `duration_seconds` | `2` | :white_check_mark: | The number of seconds to display the title popup. `0` implies forever.|
|
||||||
|
|
||||||
### Event Viewer Options
|
### Media Viewer Options
|
||||||
|
|
||||||
The `event_viewer` is used for viewing all `clip` and `snapshot` media, in a media carousel.
|
The `media_viewer` is used for viewing all `clip`, `snapshot` or recording media, in a media carousel.
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
event_viewer:
|
media_viewer:
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [fully expanded event viewer configuration example](#config-expanded-event-viewer) for how these parameters are structured.
|
See the [fully expanded Media viewer configuration example](#config-expanded-media-viewer) for how these parameters are structured.
|
||||||
|
|
||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
@@ -377,18 +377,18 @@ See the [fully expanded event viewer configuration example](#config-expanded-eve
|
|||||||
| `auto_pause` | `all` | :heavy_multiplication_x: | Whether to automatically pause events. `never` will never automatically pause, `unselected` will automatically pause when an event is unselected in the carousel, `hidden` will automatically pause when the browser/tab becomes hidden or `all` on any opportunity to automatically pause (i.e. either case).|
|
| `auto_pause` | `all` | :heavy_multiplication_x: | Whether to automatically pause events. `never` will never automatically pause, `unselected` will automatically pause when an event is unselected in the carousel, `hidden` will automatically pause when the browser/tab becomes hidden or `all` on any opportunity to automatically pause (i.e. either case).|
|
||||||
| `auto_mute` | `all` | :heavy_multiplication_x: | Whether to automatically mute events. `never` will never automatically mute, `unselected` will automatically mute when an event is unselected in the carousel, `hidden` will automatically mute when the browser/tab becomes hidden or `all` on any opportunity to automatically mute (i.e. either case).|
|
| `auto_mute` | `all` | :heavy_multiplication_x: | Whether to automatically mute events. `never` will never automatically mute, `unselected` will automatically mute when an event is unselected in the carousel, `hidden` will automatically mute when the browser/tab becomes hidden or `all` on any opportunity to automatically mute (i.e. either case).|
|
||||||
| `auto_unmute` | `never` | :heavy_multiplication_x: | Whether to automatically unmute events. `never` will never automatically unmute, `selected` will automatically unmute when an event is selected in the carousel, `visible` will automatically unmute when the browser/tab becomes visible or `all` on any opportunity to automatically unmute (i.e. either case). Note that some browsers will not allow automated unmute until the user has interacted with the page in some way -- if the user has not then the browser may pause the media instead.|
|
| `auto_unmute` | `never` | :heavy_multiplication_x: | Whether to automatically unmute events. `never` will never automatically unmute, `selected` will automatically unmute when an event is selected in the carousel, `visible` will automatically unmute when the browser/tab becomes visible or `all` on any opportunity to automatically unmute (i.e. either case). Note that some browsers will not allow automated unmute until the user has interacted with the page in some way -- if the user has not then the browser may pause the media instead.|
|
||||||
| `lazy_load` | `true` | :heavy_multiplication_x: | 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` | :heavy_multiplication_x: | Whether or not to lazily load media in the Media 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` | :heavy_multiplication_x: | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
| `draggable` | `true` | :heavy_multiplication_x: | Whether or not the Media viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
||||||
| `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between event media. Accepted values: `slide` or `none`. |
|
| `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between event media. Accepted values: `slide` or `none`. |
|
||||||
| `controls` | | :heavy_multiplication_x: | Configuration for the event viewer controls. See below. |
|
| `controls` | | :heavy_multiplication_x: | Configuration for the Media viewer controls. See below. |
|
||||||
| `actions` | | :heavy_multiplication_x: | Actions to use for all views that use the `event_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.|
|
| `actions` | | :heavy_multiplication_x: | Actions to use for all views that use the `media_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.|
|
||||||
|
|
||||||
#### Event Viewer Controls: Next / Previous
|
#### Media Viewer Controls: Next / Previous
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
event_viewer:
|
media_viewer:
|
||||||
controls:
|
controls:
|
||||||
next_previous:
|
next_previous:
|
||||||
```
|
```
|
||||||
@@ -398,12 +398,12 @@ event_viewer:
|
|||||||
| `style` | `thumbnails` | :heavy_multiplication_x: | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
| `style` | `thumbnails` | :heavy_multiplication_x: | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
||||||
| `size` | 48 | :heavy_multiplication_x: | The size of the next/previous controls in pixels. Must be >= `20`.|
|
| `size` | 48 | :heavy_multiplication_x: | The size of the next/previous controls in pixels. Must be >= `20`.|
|
||||||
|
|
||||||
#### Event Viewer Controls: Thumbnails
|
#### Media Viewer Controls: Thumbnails
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
event_viewer:
|
media_viewer:
|
||||||
controls:
|
controls:
|
||||||
thumbnails:
|
thumbnails:
|
||||||
```
|
```
|
||||||
@@ -415,19 +415,19 @@ event_viewer:
|
|||||||
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
|
||||||
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
||||||
|
|
||||||
#### Event Viewer Controls: Title
|
#### Media Viewer Controls: Title
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
event_viewer:
|
media_viewer:
|
||||||
controls:
|
controls:
|
||||||
title:
|
title:
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `mode` | `popup-bottom-right` | :heavy_multiplication_x: | How to display the event viewer media title. Acceptable values: `none`, `popup-top-left`, `popup-top-right`, `popup-bottom-left`, `popup-bottom-right` . |
|
| `mode` | `popup-bottom-right` | :heavy_multiplication_x: | How to display the Media viewer media title. Acceptable values: `none`, `popup-top-left`, `popup-top-right`, `popup-bottom-left`, `popup-bottom-right` . |
|
||||||
| `duration_seconds` | `2` | :heavy_multiplication_x: | The number of seconds to display the title popup. `0` implies forever.|
|
| `duration_seconds` | `2` | :heavy_multiplication_x: | The number of seconds to display the title popup. `0` implies forever.|
|
||||||
|
|
||||||
### Event Gallery Options
|
### Event Gallery Options
|
||||||
@@ -468,7 +468,11 @@ See the [fully expanded image configuration example](#config-expanded-image) for
|
|||||||
|
|
||||||
### Timeline Options
|
### Timeline Options
|
||||||
|
|
||||||
The `timeline` is used to show the timing sequence of events across cameras.
|
The `timeline` is used to show the timing sequence of events and recordings across cameras. You can interact with the timeline in a number of ways:
|
||||||
|
|
||||||
|
* Clicking on an event will take you to the media viewer for that event.
|
||||||
|
* Clicking on the "background", or a camera title, will take you to the recordings for that camera (seeking to the clicked time).
|
||||||
|
* Clicking on the time axis will take you to recordings for all cameras (seeking to the clicked time).
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
|
|
||||||
@@ -483,6 +487,7 @@ See the [fully expanded timeline configuration example](#config-expanded-timelin
|
|||||||
| `window_seconds` | `3600` | :heavy_multiplication_x: | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. |
|
| `window_seconds` | `3600` | :heavy_multiplication_x: | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. |
|
||||||
| `clustering_threshold` | `3` | :heavy_multiplication_x: | The number of overlapping events to allow prior to clustering/grouping them. Higher numbers cause clustering to happen less frequently. `0` disables clustering entirely.|
|
| `clustering_threshold` | `3` | :heavy_multiplication_x: | The number of overlapping events to allow prior to clustering/grouping them. Higher numbers cause clustering to happen less frequently. `0` disables clustering entirely.|
|
||||||
| `media` | `all` | :heavy_multiplication_x: | Whether to show only events with `clips`, events with `snapshots` or `all` events. When `all` is used, `clips` are favored for events that have both a clip and a snapshot.|
|
| `media` | `all` | :heavy_multiplication_x: | Whether to show only events with `clips`, events with `snapshots` or `all` events. When `all` is used, `clips` are favored for events that have both a clip and a snapshot.|
|
||||||
|
| `show_recordings` | `true` | :heavy_multiplication_x: | Whether to show recordings on the timeline (specifically: which hours have any recorded content).|
|
||||||
| `controls` | | :heavy_multiplication_x: | Configuration for the timeline controls. See below.|
|
| `controls` | | :heavy_multiplication_x: | Configuration for the timeline controls. See below.|
|
||||||
| `actions` | | :heavy_multiplication_x: | Actions to use for the `timeline` views. See [actions](#actions) below.|
|
| `actions` | | :heavy_multiplication_x: | Actions to use for the `timeline` views. See [actions](#actions) below.|
|
||||||
|
|
||||||
@@ -790,9 +795,9 @@ This card supports several different views:
|
|||||||
| ------------- | --------------------------------------------- |
|
| ------------- | --------------------------------------------- |
|
||||||
|`live` (default)| Shows the live camera view with the configured live provider.|
|
|`live` (default)| Shows the live camera view with the configured live provider.|
|
||||||
|`snapshots`|Shows an event gallery of snapshots for this camera/zone/label.|
|
|`snapshots`|Shows an event gallery of snapshots for this camera/zone/label.|
|
||||||
|`snapshot`|Shows an event viewer for the most recent snapshot for this camera/zone/label. Can also be accessed by holding down the `snapshots` menu icon.|
|
|`snapshot`|Shows a Media viewer for the most recent snapshot for this camera/zone/label. Can also be accessed by holding down the `snapshots` menu icon.|
|
||||||
|`clips`|Shows an event gallery of clips for this camera/zone/label.|
|
|`clips`|Shows an event gallery of clips for this camera/zone/label.|
|
||||||
|`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 a Media 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_seconds`).|
|
|`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.timeout_seconds`).|
|
||||||
|
|
||||||
### Navigating From A Snapshot To A Clip
|
### Navigating From A Snapshot To A Clip
|
||||||
@@ -853,7 +858,7 @@ view.
|
|||||||
| Configuration path | Views to which it refers |
|
| Configuration path | Views to which it refers |
|
||||||
| - | - |
|
| - | - |
|
||||||
| `view.actions` | All (may be overriden by the below) |
|
| `view.actions` | All (may be overriden by the below) |
|
||||||
| `event_viewer.actions` | `clip`, `snapshot` |
|
| `media_viewer.actions` | `clip`, `snapshot` |
|
||||||
| `event_gallery.actions` | `clips`, `snapshots` |
|
| `event_gallery.actions` | `clips`, `snapshots` |
|
||||||
| `live.actions` | `live` |
|
| `live.actions` | `live` |
|
||||||
| `image.actions` | `image` |
|
| `image.actions` | `image` |
|
||||||
@@ -951,6 +956,24 @@ View events in the timeline.
|
|||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/timeline.gif" alt="Event Timeline" width="400px">
|
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/timeline.gif" alt="Event Timeline" width="400px">
|
||||||
|
|
||||||
|
### Single Camera Recordings
|
||||||
|
|
||||||
|
View recordings for a camera across time:
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/recording-seek.gif" alt="Recording for single camera" width="400px">
|
||||||
|
|
||||||
|
### Multiple Camera Recordings
|
||||||
|
|
||||||
|
View recordings for multiple cameras at a given time:
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/recording-seek-all-cameras.gif" alt="Recording for multiple cameras" width="400px">
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
|
||||||
|
Dim the card when not used.
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/dark-mode.gif" alt="Card dark mode" width="400px">
|
||||||
|
|
||||||
<a name="screenshots-card-casting"></a>
|
<a name="screenshots-card-casting"></a>
|
||||||
|
|
||||||
### Card Casting
|
### Card Casting
|
||||||
@@ -1185,15 +1208,15 @@ live:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<a name="config-expanded-event-viewer"></a>
|
<a name="config-expanded-media-viewer"></a>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand: Event Viewer section</summary>
|
<summary>Expand: Media Viewer section</summary>
|
||||||
|
|
||||||
Reference: [Event Viewer Options](#event-viewer-options).
|
Reference: [Media Viewer Options](#event-viewer-options).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
event_viewer:
|
media_viewer:
|
||||||
auto_play: all
|
auto_play: all
|
||||||
auto_pause: all
|
auto_pause: all
|
||||||
auto_mute: all
|
auto_mute: all
|
||||||
@@ -1617,6 +1640,7 @@ Reference: [Timeline Options](#timeline-options).
|
|||||||
timeline:
|
timeline:
|
||||||
clustering_threshold: 3
|
clustering_threshold: 3
|
||||||
media: all
|
media: all
|
||||||
|
show_recordings: true
|
||||||
window_seconds: 3600
|
window_seconds: 3600
|
||||||
controls:
|
controls:
|
||||||
thumbnails:
|
thumbnails:
|
||||||
@@ -2559,6 +2583,14 @@ You must be using a version of the [Frigate integration](https://github.com/blak
|
|||||||
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.
|
||||||
|
|
||||||
|
|
||||||
|
### Timeline shows error message
|
||||||
|
|
||||||
|
If the timeline shows a message such as `Failed to receive response from Home
|
||||||
|
Assistant for request.` make sure you are running a version of the [Frigate
|
||||||
|
integration](https://github.com/blakeblackshear/frigate-hass-integration) >=
|
||||||
|
3.0.0.
|
||||||
|
|
||||||
### 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
|
||||||
@@ -2602,7 +2634,7 @@ Dragging the Safari video controls "progress bar" conflicts with carousel "dragg
|
|||||||
```yaml
|
```yaml
|
||||||
live:
|
live:
|
||||||
draggable: false
|
draggable: false
|
||||||
event_viewer:
|
media_viewer:
|
||||||
draggable: false
|
draggable: false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
+11
-11
@@ -5,7 +5,7 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
ConditionState,
|
ConditionState,
|
||||||
conditionStateRequestHandler,
|
conditionStateRequestHandler,
|
||||||
getOverriddenConfig,
|
getOverriddenConfig,
|
||||||
getOverridesByKey,
|
getOverridesByKey
|
||||||
} from './card-condition.js';
|
} from './card-condition.js';
|
||||||
import './components/elements.js';
|
import './components/elements.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
CAMERA_BIRDSEYE,
|
CAMERA_BIRDSEYE,
|
||||||
CARD_VERSION,
|
CARD_VERSION,
|
||||||
MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA,
|
MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA,
|
||||||
REPO_URL,
|
REPO_URL
|
||||||
} from './const.js';
|
} from './const.js';
|
||||||
import './editor.js';
|
import './editor.js';
|
||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
@@ -63,14 +63,14 @@ import {
|
|||||||
MediaShowInfo,
|
MediaShowInfo,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
Message,
|
Message,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
createFrigateCardCustomAction,
|
createFrigateCardCustomAction,
|
||||||
frigateCardHandleAction,
|
frigateCardHandleAction,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction
|
||||||
} from './utils/action.js';
|
} from './utils/action.js';
|
||||||
import { contentsChanged } from './utils/basic.js';
|
import { contentsChanged } from './utils/basic.js';
|
||||||
import { getCameraIcon, getCameraID, getCameraTitle } from './utils/camera.js';
|
import { getCameraIcon, getCameraID, getCameraTitle } from './utils/camera.js';
|
||||||
@@ -81,14 +81,14 @@ import {
|
|||||||
homeAssistantSignPath,
|
homeAssistantSignPath,
|
||||||
isHassDifferent,
|
isHassDifferent,
|
||||||
isTriggeredState,
|
isTriggeredState,
|
||||||
sideLoadHomeAssistantElements,
|
sideLoadHomeAssistantElements
|
||||||
} from './utils/ha';
|
} from './utils/ha';
|
||||||
import { getEventID } from './utils/ha/browse-media.js';
|
import { getEventID } from './utils/ha/browse-media.js';
|
||||||
import {
|
import {
|
||||||
ExtendedEntityCache,
|
ExtendedEntityCache,
|
||||||
getAllEntities,
|
getAllEntities,
|
||||||
getExtendedEntities,
|
getExtendedEntities,
|
||||||
getExtendedEntity,
|
getExtendedEntity
|
||||||
} from './utils/ha/entity-registry.js';
|
} from './utils/ha/entity-registry.js';
|
||||||
import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
|
import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
|
||||||
import { supportsFeature } from './utils/ha/update.js';
|
import { supportsFeature } from './utils/ha/update.js';
|
||||||
@@ -975,7 +975,7 @@ export class FrigateCard extends LitElement {
|
|||||||
* @returns A boolean indicating whether the camera was changed.
|
* @returns A boolean indicating whether the camera was changed.
|
||||||
*/
|
*/
|
||||||
protected _updateTriggeredCameras(oldHass: HomeAssistant): boolean {
|
protected _updateTriggeredCameras(oldHass: HomeAssistant): boolean {
|
||||||
if (!this._view) {
|
if (!this._view || !this._isAutomatedViewUpdateAllowed(true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,7 +1001,7 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggerChanges && this._isAutomatedViewUpdateAllowed(true)) {
|
if (triggerChanges) {
|
||||||
if (!this._triggers.size) {
|
if (!this._triggers.size) {
|
||||||
this._changeView();
|
this._changeView();
|
||||||
changedCamera = true;
|
changedCamera = true;
|
||||||
@@ -1545,7 +1545,7 @@ export class FrigateCard extends LitElement {
|
|||||||
} else if (this._view?.isGalleryView()) {
|
} else if (this._view?.isGalleryView()) {
|
||||||
specificActions = this._getConfig().event_gallery?.actions;
|
specificActions = this._getConfig().event_gallery?.actions;
|
||||||
} else if (this._view?.isViewerView()) {
|
} else if (this._view?.isViewerView()) {
|
||||||
specificActions = this._getConfig().event_viewer.actions;
|
specificActions = this._getConfig().media_viewer.actions;
|
||||||
} else if (this._view?.is('image')) {
|
} else if (this._view?.is('image')) {
|
||||||
specificActions = this._getConfig().image?.actions;
|
specificActions = this._getConfig().image?.actions;
|
||||||
} else if (this._view?.is('timeline')) {
|
} else if (this._view?.is('timeline')) {
|
||||||
@@ -1678,7 +1678,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameras=${this._cameras}
|
.cameras=${this._cameras}
|
||||||
.viewerConfig=${this._getConfig().event_viewer}
|
.viewerConfig=${this._getConfig().media_viewer}
|
||||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
>
|
>
|
||||||
</frigate-card-viewer>`
|
</frigate-card-viewer>`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ConditionState, fetchStateAndEvaluateCondition } from '../card-conditio
|
|||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import elementsStyle from '../scss/elements.scss';
|
import elementsStyle from '../scss/elements.scss';
|
||||||
import {
|
import {
|
||||||
|
FrigateCardError,
|
||||||
FrigateConditional,
|
FrigateConditional,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
PictureElements
|
PictureElements
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { dispatchErrorMessageEvent } from './message.js';
|
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||||
|
|
||||||
/* A note on picture element rendering:
|
/* A note on picture element rendering:
|
||||||
*
|
*
|
||||||
@@ -108,7 +109,7 @@ export class FrigateCardElementsCore extends LitElement {
|
|||||||
element.setConfig(config);
|
element.setConfig(config);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e, (e as Error).stack);
|
console.error(e, (e as Error).stack);
|
||||||
throw new Error(localize('error.invalid_elements_config'));
|
throw new FrigateCardError(localize('error.invalid_elements_config'));
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,7 @@ export class FrigateCardElementsCore extends LitElement {
|
|||||||
this._root = this._createRoot();
|
this._root = this._createRoot();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(this, (e as Error).message);
|
return dispatchFrigateCardErrorEvent(this, e as FrigateCardError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-12
@@ -11,7 +11,12 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import galleryStyle from '../scss/gallery.scss';
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
import { CameraConfig, frigateCardConfigDefaults, GalleryConfig } from '../types.js';
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
frigateCardConfigDefaults,
|
||||||
|
GalleryConfig,
|
||||||
|
THUMBNAIL_WIDTH_MAX
|
||||||
|
} from '../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
import {
|
import {
|
||||||
fetchChildMediaAndDispatchViewChange,
|
fetchChildMediaAndDispatchViewChange,
|
||||||
@@ -135,20 +140,28 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle gallery resize.
|
* Set gallery columns.
|
||||||
*/
|
*/
|
||||||
protected _resizeHandler(): void {
|
protected _setColumnCount(): void {
|
||||||
const thumbnailSize =
|
const thumbnailSize =
|
||||||
this.galleryConfig?.controls.thumbnails.size ??
|
this.galleryConfig?.controls.thumbnails.size ??
|
||||||
frigateCardConfigDefaults.event_gallery.controls.thumbnails.size;
|
frigateCardConfigDefaults.event_gallery.controls.thumbnails.size;
|
||||||
this.style.setProperty(
|
const columns = this.galleryConfig?.controls.thumbnails.show_details
|
||||||
'--frigate-card-gallery-columns',
|
? Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN))
|
||||||
String(
|
: Math.max(
|
||||||
!this.galleryConfig?.controls.thumbnails.show_details
|
1,
|
||||||
? Math.round(this.clientWidth / thumbnailSize)
|
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
|
||||||
: Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN)),
|
Math.ceil(this.clientWidth / thumbnailSize),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
|
this.style.setProperty('--frigate-card-gallery-columns', String(columns));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle gallery resize.
|
||||||
|
*/
|
||||||
|
protected _resizeHandler(): void {
|
||||||
|
this._setColumnCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,6 +187,7 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
this.removeAttribute('details');
|
this.removeAttribute('details');
|
||||||
}
|
}
|
||||||
|
this._setColumnCount();
|
||||||
if (this.galleryConfig?.controls.thumbnails.size) {
|
if (this.galleryConfig?.controls.thumbnails.size) {
|
||||||
this.style.setProperty(
|
this.style.setProperty(
|
||||||
'--frigate-card-thumbnail-size',
|
'--frigate-card-thumbnail-size',
|
||||||
@@ -230,7 +244,6 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
stopEventFromActivatingCardWideActions(ev);
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
}}
|
}}
|
||||||
outlined=""
|
outlined=""
|
||||||
class="foo"
|
|
||||||
>
|
>
|
||||||
<div>${child.title}</div>
|
<div>${child.title}</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
|
|||||||
+34
-1
@@ -612,6 +612,13 @@ export class FrigateCardLiveProvider extends LitElement {
|
|||||||
this._providerRef.value?.unmute();
|
this._providerRef.value?.unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
this._providerRef.value?.seek(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
protected _getResolvedProvider(): LiveProvider {
|
protected _getResolvedProvider(): LiveProvider {
|
||||||
if (this.cameraConfig?.live_provider === 'auto') {
|
if (this.cameraConfig?.live_provider === 'auto') {
|
||||||
if (
|
if (
|
||||||
@@ -711,6 +718,13 @@ export class FrigateCardLiveFrigate extends LitElement {
|
|||||||
this._playerRef.value?.unmute();
|
this._playerRef.value?.unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
this._playerRef.value?.seek(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -810,6 +824,16 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
const player = this._getPlayer();
|
||||||
|
if (player) {
|
||||||
|
player.currentTime = seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the underlying video player.
|
* Get the underlying video player.
|
||||||
* @returns The player or `null` if not found.
|
* @returns The player or `null` if not found.
|
||||||
@@ -867,8 +891,9 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
|
|||||||
return dispatchErrorMessageEvent(
|
return dispatchErrorMessageEvent(
|
||||||
this,
|
this,
|
||||||
e instanceof FrigateCardError
|
e instanceof FrigateCardError
|
||||||
? (e as FrigateCardError).message
|
? e.message
|
||||||
: localize('error.webrtc_card_reported_error') + ': ' + (e as Error).message,
|
: localize('error.webrtc_card_reported_error') + ': ' + (e as Error).message,
|
||||||
|
(e as FrigateCardError).context
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return html`${webrtcElement}`;
|
return html`${webrtcElement}`;
|
||||||
@@ -964,6 +989,14 @@ export class FrigateCardLiveJSMPEG extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video (unsupported).
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public seek(_seconds: number): void {
|
||||||
|
// JSMPEG does not support seeking.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a signed player URL.
|
* Get a signed player URL.
|
||||||
* @returns A URL or null.
|
* @returns A URL or null.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { TROUBLESHOOTING_URL } from '../const.js';
|
import { TROUBLESHOOTING_URL } from '../const.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import messageStyle from '../scss/message.scss';
|
import messageStyle from '../scss/message.scss';
|
||||||
import { Message } from '../types.js';
|
import { FrigateCardError, Message } from '../types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
|
|
||||||
@customElement('frigate-card-message')
|
@customElement('frigate-card-message')
|
||||||
@@ -140,3 +140,19 @@ export function dispatchErrorMessageEvent(
|
|||||||
context: context,
|
context: context,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch an event with an error message to show to the user.
|
||||||
|
* @param element The element to send the event.
|
||||||
|
* @param message The message to show.
|
||||||
|
*/
|
||||||
|
export function dispatchFrigateCardErrorEvent(
|
||||||
|
element: HTMLElement,
|
||||||
|
error: FrigateCardError
|
||||||
|
): void {
|
||||||
|
dispatchFrigateCardEvent<Message>(element, 'message', {
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
context: error.context || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import surroundThumbnailsStyle from '../scss/surround.scss';
|
|||||||
import {
|
import {
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
|
FrigateCardError,
|
||||||
FrigateCardView,
|
FrigateCardView,
|
||||||
ThumbnailsControlConfig
|
ThumbnailsControlConfig
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
multipleBrowseMediaQueryMerged
|
multipleBrowseMediaQueryMerged
|
||||||
} from '../utils/ha/browse-media';
|
} from '../utils/ha/browse-media';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { dispatchErrorMessageEvent } from './message.js';
|
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
try {
|
try {
|
||||||
parent = await multipleBrowseMediaQueryMerged(this.hass, this.browseMediaParams);
|
parent = await multipleBrowseMediaQueryMerged(this.hass, this.browseMediaParams);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(this, (e as Error).message);
|
return dispatchFrigateCardErrorEvent(this, e as FrigateCardError);
|
||||||
}
|
}
|
||||||
if (getFirstTrueMediaChildIndex(parent) !== null) {
|
if (getFirstTrueMediaChildIndex(parent) !== null) {
|
||||||
this.view
|
this.view
|
||||||
@@ -144,13 +145,16 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
.selected=${this.view.childIndex}
|
.selected=${this.view.childIndex}
|
||||||
@frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
@frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
||||||
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||||
|
// Send the view change from the source of the tap event, so the
|
||||||
|
// view change will be caught by the handler above (to close the drawer).
|
||||||
this.view
|
this.view
|
||||||
?.evolve({
|
?.evolve({
|
||||||
view: this.targetView || 'event',
|
view: this.targetView || 'media',
|
||||||
target: ev.detail.target,
|
target: ev.detail.target,
|
||||||
childIndex: ev.detail.childIndex,
|
childIndex: ev.detail.childIndex,
|
||||||
|
context: null,
|
||||||
})
|
})
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(ev.composedPath()[0]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
</frigate-card-thumbnail-carousel>`
|
</frigate-card-thumbnail-carousel>`
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { EmblaOptionsType } from 'embla-carousel';
|
|||||||
import { CSSResultGroup, html, PropertyValues, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, PropertyValues, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
||||||
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||||
import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
@@ -120,6 +119,21 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
|||||||
return slides;
|
return slides;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an update will occur.
|
||||||
|
* @param changedProps The changed properties
|
||||||
|
*/
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has('_config')) {
|
||||||
|
if (this._config?.size) {
|
||||||
|
this.style.setProperty(
|
||||||
|
'--frigate-card-thumbnail-size',
|
||||||
|
`${this._config.size}px`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The updated lifecycle callback for this element.
|
* The updated lifecycle callback for this element.
|
||||||
* @param changedProperties The properties that were changed in this render.
|
* @param changedProperties The properties that were changed in this render.
|
||||||
@@ -170,7 +184,6 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
|||||||
.childIndex=${childIndex}
|
.childIndex=${childIndex}
|
||||||
?details=${this._config?.show_details}
|
?details=${this._config?.show_details}
|
||||||
?controls=${this._config?.show_controls}
|
?controls=${this._config?.show_controls}
|
||||||
thumbnail_size=${ifDefined(this._config?.size)}
|
|
||||||
class="${classMap(classes)}"
|
class="${classMap(classes)}"
|
||||||
@click=${(ev) => {
|
@click=${(ev) => {
|
||||||
if (this._carousel && this._carousel.clickAllowed()) {
|
if (this._carousel && this._carousel.clickAllowed()) {
|
||||||
|
|||||||
+133
-36
@@ -3,8 +3,14 @@ import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
||||||
|
import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss';
|
||||||
|
import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording.scss';
|
||||||
import thumbnailStyle from '../scss/thumbnail.scss';
|
import thumbnailStyle from '../scss/thumbnail.scss';
|
||||||
import type { FrigateBrowseMediaSource, FrigateEvent } from '../types.js';
|
import type {
|
||||||
|
FrigateBrowseMediaSource,
|
||||||
|
FrigateEvent,
|
||||||
|
FrigateRecording
|
||||||
|
} from '../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
import { prettifyTitle } from '../utils/basic.js';
|
import { prettifyTitle } from '../utils/basic.js';
|
||||||
import { getEventDurationString } from '../utils/ha/browse-media.js';
|
import { getEventDurationString } from '../utils/ha/browse-media.js';
|
||||||
@@ -13,8 +19,49 @@ import { View } from '../view.js';
|
|||||||
// The minimum width of a thumbnail with details enabled.
|
// The minimum width of a thumbnail with details enabled.
|
||||||
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
||||||
|
|
||||||
@customElement('frigate-card-thumbnail-details')
|
@customElement('frigate-card-thumbnail-feature-event')
|
||||||
export class FrigateCardThumbnailDetails extends LitElement {
|
export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public thumbnail?: string;
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html`
|
||||||
|
${this.thumbnail
|
||||||
|
? html`<img src="${this.thumbnail}" />`
|
||||||
|
: html`<ha-icon
|
||||||
|
icon="mdi:image-off"
|
||||||
|
title=${localize('thumbnail.no_thumbnail')}
|
||||||
|
></ha-icon> `}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return unsafeCSS(thumbnailFeatureEventStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-thumbnail-feature-recording')
|
||||||
|
export class FrigateCardThumbnailFeatureRecording extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public date?: Date;
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="title">${format(this.date, 'HH:mm')}</div>
|
||||||
|
<div class="subtitle">${format(this.date, 'MMM do')}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return unsafeCSS(thumbnailFeatureRecordingStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-thumbnail-details-event')
|
||||||
|
export class FrigateCardThumbnailDetailsEvent extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public event?: FrigateEvent;
|
public event?: FrigateEvent;
|
||||||
|
|
||||||
@@ -39,9 +86,35 @@ export class FrigateCardThumbnailDetails extends LitElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static get styles(): CSSResult {
|
||||||
* Get element styles.
|
return unsafeCSS(thumbnailDetailsStyle);
|
||||||
*/
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-thumbnail-details-recording')
|
||||||
|
export class FrigateCardThumbnailDetailsRecording extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public recording?: FrigateRecording;
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.recording) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html`<div class="left">
|
||||||
|
<div class="larger">${prettifyTitle(this.recording.camera) || ''}</div>
|
||||||
|
${this.recording.seek_time
|
||||||
|
? html` <div>
|
||||||
|
<span class="heading">${localize('recording.seek')}</span>
|
||||||
|
<span>${format(fromUnixTime(this.recording.seek_time), 'HH:mm:ss')}</span>
|
||||||
|
</div>`
|
||||||
|
: html``}
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<span class="larger">${this.recording.events}</span>
|
||||||
|
<span>${localize('recording.events')}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return unsafeCSS(thumbnailDetailsStyle);
|
return unsafeCSS(thumbnailDetailsStyle);
|
||||||
}
|
}
|
||||||
@@ -55,11 +128,6 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
@property({ attribute: true, type: Boolean })
|
@property({ attribute: true, type: Boolean })
|
||||||
public controls = false;
|
public controls = false;
|
||||||
|
|
||||||
@property({ attribute: true, type: Number })
|
|
||||||
set thumbnail_size(size: number) {
|
|
||||||
this.style.setProperty('--frigate-card-thumbnail-size', `${size}px`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Data-binding based interface
|
// Data-binding based interface
|
||||||
// ============================
|
// ============================
|
||||||
@@ -90,6 +158,7 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
let event: FrigateEvent | null = null;
|
let event: FrigateEvent | null = null;
|
||||||
|
let recording: FrigateRecording | null = null;
|
||||||
let thumbnail: string | null = null;
|
let thumbnail: string | null = null;
|
||||||
let label: string | null = null;
|
let label: string | null = null;
|
||||||
|
|
||||||
@@ -97,6 +166,7 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
if (this.target && this.target.children && this.childIndex !== undefined) {
|
if (this.target && this.target.children && this.childIndex !== undefined) {
|
||||||
const media = this.target.children[this.childIndex];
|
const media = this.target.children[this.childIndex];
|
||||||
event = media.frigate?.event ?? null;
|
event = media.frigate?.event ?? null;
|
||||||
|
recording = media.frigate?.recording ?? null;
|
||||||
thumbnail = media.thumbnail;
|
thumbnail = media.thumbnail;
|
||||||
label = media.title;
|
label = media.title;
|
||||||
}
|
}
|
||||||
@@ -108,34 +178,46 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
thumbnail = this.thumbnail ? this.thumbnail : thumbnail;
|
thumbnail = this.thumbnail ? this.thumbnail : thumbnail;
|
||||||
label = this.label ? this.label : label;
|
label = this.label ? this.label : label;
|
||||||
|
|
||||||
if (!thumbnail) {
|
if (!event && !recording) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` <img
|
return html` ${event
|
||||||
aria-label="${label ?? ''}"
|
? html`<frigate-card-thumbnail-feature-event
|
||||||
src="${thumbnail}"
|
aria-label="${label ?? ''}"
|
||||||
title="${label ?? ''}"
|
title="${label ?? ''}"
|
||||||
/>
|
.thumbnail=${thumbnail ?? undefined}
|
||||||
${this.controls && event?.retain_indefinitely
|
.label=${label ?? undefined}
|
||||||
? html` <ha-icon
|
></frigate-card-thumbnail-feature-event>`
|
||||||
|
: html`<frigate-card-thumbnail-feature-recording
|
||||||
|
aria-label="${label ?? ''}"
|
||||||
|
title="${label ?? ''}"
|
||||||
|
.date=${recording ? fromUnixTime(recording.start_time) : undefined}
|
||||||
|
></frigate-card-thumbnail-feature-recording>`}
|
||||||
|
${this.controls && event?.retain_indefinitely
|
||||||
|
? html` <ha-icon
|
||||||
class="favorite"
|
class="favorite"
|
||||||
icon="mdi:star"
|
icon="mdi:star"
|
||||||
title=${localize('thumbnail.retain_indefinitely')}
|
title=${localize('thumbnail.retain_indefinitely')}
|
||||||
/>`
|
/></ha-icon>`
|
||||||
: ``}
|
: ``}
|
||||||
${this.details && event
|
${this.details && event
|
||||||
? html`<frigate-card-thumbnail-details
|
? html`<frigate-card-thumbnail-details-event
|
||||||
.event=${event}
|
.event=${event ?? undefined}
|
||||||
></frigate-card-thumbnail-details>`
|
></frigate-card-thumbnail-details-event>`
|
||||||
: html``}
|
: this.details && recording
|
||||||
${this.controls
|
? html`<frigate-card-thumbnail-details-recording
|
||||||
? html`<ha-icon
|
.recording=${recording ?? undefined}
|
||||||
class="timeline"
|
></frigate-card-thumbnail-details-recording>`
|
||||||
icon="mdi:target"
|
: html``}
|
||||||
title=${localize('thumbnail.timeline')}
|
${this.controls
|
||||||
@click=${(ev: Event) => {
|
? html`<ha-icon
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
class="timeline"
|
||||||
|
icon="mdi:target"
|
||||||
|
title=${localize('thumbnail.timeline')}
|
||||||
|
@click=${(ev: Event) => {
|
||||||
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
|
if (event) {
|
||||||
this.view
|
this.view
|
||||||
?.evolve({
|
?.evolve({
|
||||||
view: 'timeline',
|
view: 'timeline',
|
||||||
@@ -144,9 +226,24 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
context: {},
|
context: {},
|
||||||
})
|
})
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(this);
|
||||||
}}
|
} else if (recording) {
|
||||||
></ha-icon>`
|
this.view
|
||||||
: ''}`;
|
?.evolve({
|
||||||
|
view: 'timeline',
|
||||||
|
target: null,
|
||||||
|
childIndex: null,
|
||||||
|
context: {
|
||||||
|
window: {
|
||||||
|
start: fromUnixTime(recording.start_time),
|
||||||
|
end: fromUnixTime(recording.end_time),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.dispatchChangeEvent(this);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></ha-icon>`
|
||||||
|
: ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+385
-82
@@ -1,5 +1,13 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { add, fromUnixTime, sub } from 'date-fns';
|
import {
|
||||||
|
add,
|
||||||
|
endOfHour,
|
||||||
|
format,
|
||||||
|
fromUnixTime,
|
||||||
|
getUnixTime,
|
||||||
|
startOfHour,
|
||||||
|
sub
|
||||||
|
} from 'date-fns';
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
@@ -29,32 +37,44 @@ import timelineStyle from '../scss/timeline.scss';
|
|||||||
import {
|
import {
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
|
FrigateCardError,
|
||||||
FrigateEvent,
|
FrigateEvent,
|
||||||
TimelineConfig
|
TimelineConfig
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent, prettifyTitle } from '../utils/basic';
|
||||||
import { getCameraTitle } from '../utils/camera.js';
|
import { getCameraTitle } from '../utils/camera.js';
|
||||||
|
import {
|
||||||
|
getRecordingSegments,
|
||||||
|
getRecordingsSummary,
|
||||||
|
RecordingSegments,
|
||||||
|
RecordingSummary
|
||||||
|
} from '../utils/frigate';
|
||||||
import {
|
import {
|
||||||
createEventParentForChildren,
|
createEventParentForChildren,
|
||||||
|
createVideoChild,
|
||||||
|
generateRecordingIdentifier,
|
||||||
getBrowseMediaQueryParameters,
|
getBrowseMediaQueryParameters,
|
||||||
isTrueMedia,
|
isTrueMedia,
|
||||||
multipleBrowseMediaQuery
|
multipleBrowseMediaQuery
|
||||||
} from '../utils/ha/browse-media';
|
} from '../utils/ha/browse-media';
|
||||||
import { View, ViewContext } from '../view';
|
import { View, ViewContext } from '../view';
|
||||||
import { dispatchErrorMessageEvent, dispatchMessageEvent } from './message.js';
|
import { dispatchFrigateCardErrorEvent, dispatchMessageEvent } from './message.js';
|
||||||
import './surround-thumbnails.js';
|
import './surround-thumbnails.js';
|
||||||
|
|
||||||
const TIMELINE_EVENT_MANAGER_MAX_AGE_SECONDS = 10;
|
const TIMELINE_DATA_MANAGER_MAX_AGE_SECONDS = 10;
|
||||||
|
|
||||||
interface FrigateCardGroupData {
|
interface FrigateCardGroupData {
|
||||||
id: string;
|
id: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
interface FrigateCardTimelineItem extends TimelineItem {
|
interface FrigateCardTimelineItem extends TimelineItem {
|
||||||
event: FrigateEvent;
|
start: number;
|
||||||
|
end?: number;
|
||||||
|
event?: FrigateEvent;
|
||||||
source?: FrigateBrowseMediaSource;
|
source?: FrigateBrowseMediaSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +88,17 @@ interface TimelineViewContext extends ViewContext {
|
|||||||
|
|
||||||
type TimelineMediaType = 'all' | 'clips' | 'snapshots';
|
type TimelineMediaType = 'all' | 'clips' | 'snapshots';
|
||||||
|
|
||||||
|
interface CameraRecordings {
|
||||||
|
segments: RecordingSegments;
|
||||||
|
summary: RecordingSummary;
|
||||||
|
}
|
||||||
|
|
||||||
const isHoverableDevice = window.matchMedia('(hover: hover) and (pointer: fine)');
|
const isHoverableDevice = window.matchMedia('(hover: hover) and (pointer: fine)');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A manager to maintain/fetch timeline events.
|
* A manager to maintain/fetch timeline events.
|
||||||
*/
|
*/
|
||||||
class TimelineEventManager {
|
class TimelineDataManager {
|
||||||
protected _dataset = new DataSet<FrigateCardTimelineItem>();
|
protected _dataset = new DataSet<FrigateCardTimelineItem>();
|
||||||
|
|
||||||
// The earliest date managed.
|
// The earliest date managed.
|
||||||
@@ -87,7 +112,7 @@ class TimelineEventManager {
|
|||||||
|
|
||||||
// The maximum allowable age of fetch data (will not fetch more frequently
|
// The maximum allowable age of fetch data (will not fetch more frequently
|
||||||
// than this).
|
// than this).
|
||||||
protected _maxAgeSeconds: number = TIMELINE_EVENT_MANAGER_MAX_AGE_SECONDS;
|
protected _maxAgeSeconds: number = TIMELINE_DATA_MANAGER_MAX_AGE_SECONDS;
|
||||||
|
|
||||||
protected _contentCallback?: (source: FrigateBrowseMediaSource) => string;
|
protected _contentCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
protected _tooltipCallback?: (source: FrigateBrowseMediaSource) => string;
|
protected _tooltipCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
@@ -183,9 +208,7 @@ class TimelineEventManager {
|
|||||||
* @param end An optional end of the date range.
|
* @param end An optional end of the date range.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public hasCoverage(start: Date, end?: Date): boolean {
|
public hasCoverage(now: Date, start: Date, end?: Date): boolean {
|
||||||
const now = new Date().getTime();
|
|
||||||
|
|
||||||
// Never fetched: no coverage.
|
// Never fetched: no coverage.
|
||||||
if (!this._dateFetch || !this._dateStart || !this._dateEnd) {
|
if (!this._dateFetch || !this._dateStart || !this._dateEnd) {
|
||||||
return false;
|
return false;
|
||||||
@@ -194,7 +217,7 @@ class TimelineEventManager {
|
|||||||
// If the most recent fetch is older than maxAgeSeconds: no coverage.
|
// If the most recent fetch is older than maxAgeSeconds: no coverage.
|
||||||
if (
|
if (
|
||||||
this._maxAgeSeconds &&
|
this._maxAgeSeconds &&
|
||||||
now - this._dateFetch.getTime() > this._maxAgeSeconds * 1000
|
now.getTime() - this._dateFetch.getTime() > this._maxAgeSeconds * 1000
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -219,7 +242,7 @@ class TimelineEventManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// If the requested end time is beyond `_maxAgeSeconds` of now: no coverage.
|
// If the requested end time is beyond `_maxAgeSeconds` of now: no coverage.
|
||||||
if (now - end.getTime() > this._maxAgeSeconds * 1000) {
|
if (now.getTime() - end.getTime() > this._maxAgeSeconds * 1000) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,21 +260,109 @@ class TimelineEventManager {
|
|||||||
* @param end Fetch events that start earlier than this date.
|
* @param end Fetch events that start earlier than this date.
|
||||||
* @returns `true` if events were fetched, `false` otherwise.
|
* @returns `true` if events were fetched, `false` otherwise.
|
||||||
*/
|
*/
|
||||||
public async fetchEventsIfNecessary(
|
public async fetchIfNecessary(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: ExtendedHomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
media: TimelineMediaType,
|
eventMedia: TimelineMediaType,
|
||||||
start: Date,
|
start: Date,
|
||||||
end: Date,
|
end: Date,
|
||||||
|
recordings?: boolean,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (this.hasCoverage(start, end)) {
|
// Cannot fetch the future, always clip the end date to now so as to avoid
|
||||||
|
// checking for coverage that could not possibly exist yet.
|
||||||
|
const now = new Date();
|
||||||
|
end = end > now ? now : end;
|
||||||
|
|
||||||
|
if (this.hasCoverage(now, start, end)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await this._fetchEvents(element, hass, cameras, media, start, end);
|
|
||||||
|
if (!this._dateStart || start < this._dateStart) {
|
||||||
|
this._dateStart = start;
|
||||||
|
}
|
||||||
|
if (!this._dateEnd || end > this._dateEnd) {
|
||||||
|
this._dateEnd = end;
|
||||||
|
}
|
||||||
|
this._dateFetch = new Date();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
// Events are always fetched for the maximum extent of the managed
|
||||||
|
// range. This is because events may change at any point in time
|
||||||
|
// (e.g. a long-running event that ends).
|
||||||
|
this._fetchEvents(
|
||||||
|
element,
|
||||||
|
hass,
|
||||||
|
cameras,
|
||||||
|
eventMedia,
|
||||||
|
this._dateStart,
|
||||||
|
this._dateEnd,
|
||||||
|
),
|
||||||
|
...(recordings ? [this._fetchRecordings(element, hass, cameras)] : []),
|
||||||
|
]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch recording hours for the timeline.
|
||||||
|
* @param element The element to send error events from.
|
||||||
|
* @param hass The HomeAssistant object.
|
||||||
|
* @param cameras The cameras map.
|
||||||
|
* @param start Fetch events that start later than this date.
|
||||||
|
* @param end Fetch events that start earlier than this date.
|
||||||
|
*/
|
||||||
|
protected async _fetchRecordings(
|
||||||
|
element: HTMLElement,
|
||||||
|
hass: ExtendedHomeAssistant,
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
): Promise<void> {
|
||||||
|
const items: FrigateCardTimelineItem[] = [];
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const storeRecordings = async (
|
||||||
|
camera: string,
|
||||||
|
config: CameraConfig,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!config.camera_name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let summary: RecordingSummary;
|
||||||
|
try {
|
||||||
|
summary = await getRecordingsSummary(hass, config.client_id, config.camera_name);
|
||||||
|
} catch (e) {
|
||||||
|
return dispatchFrigateCardErrorEvent(element, e as FrigateCardError);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dayData of summary) {
|
||||||
|
for (const hourData of dayData.hours) {
|
||||||
|
const hour = add(dayData.day, { hours: hourData.hour });
|
||||||
|
const endHour = endOfHour(hour);
|
||||||
|
items.push({
|
||||||
|
id: `recording-${camera}-${format(hour, 'yyyy-MM-dd-HH')}`,
|
||||||
|
group: camera,
|
||||||
|
start: getUnixTime(startOfHour(hour)) * 1000,
|
||||||
|
|
||||||
|
// Don't let the recordings show off into the future (even though it
|
||||||
|
// is intended to be indicative of any recordings within that hour
|
||||||
|
// -- it still looks strange!)
|
||||||
|
end: (endHour > now ? getUnixTime(now) : getUnixTime(endHour)) * 1000,
|
||||||
|
type: 'background',
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(cameras.entries()).map(([camera, config]: [string, CameraConfig]) =>
|
||||||
|
storeRecordings(camera, config),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this._dataset.update(items);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch events for the timeline.
|
* Fetch events for the timeline.
|
||||||
* @param element The element to send error events from.
|
* @param element The element to send error events from.
|
||||||
@@ -265,34 +376,16 @@ class TimelineEventManager {
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
media: TimelineMediaType,
|
media: TimelineMediaType,
|
||||||
start?: Date,
|
start: Date,
|
||||||
end?: Date,
|
end: Date,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this._dateStart || (start && start < this._dateStart)) {
|
|
||||||
this._dateStart = start;
|
|
||||||
}
|
|
||||||
if (!this._dateEnd || (end && end > this._dateEnd)) {
|
|
||||||
this._dateEnd = end;
|
|
||||||
}
|
|
||||||
if (!this._dateStart || !this._dateEnd) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._dateFetch = new Date();
|
|
||||||
|
|
||||||
const params: BrowseMediaQueryParameters[] = [];
|
const params: BrowseMediaQueryParameters[] = [];
|
||||||
cameras.forEach((cameraConfig, cameraID) => {
|
cameras.forEach((cameraConfig, cameraID) => {
|
||||||
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) => {
|
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) => {
|
||||||
if (
|
if (cameraConfig.camera_name !== CAMERA_BIRDSEYE) {
|
||||||
this._dateEnd &&
|
|
||||||
this._dateStart &&
|
|
||||||
cameraConfig.camera_name !== CAMERA_BIRDSEYE
|
|
||||||
) {
|
|
||||||
const param = getBrowseMediaQueryParameters(hass, cameraID, cameraConfig, {
|
const param = getBrowseMediaQueryParameters(hass, cameraID, cameraConfig, {
|
||||||
// Events are always fetched for the maximum extent of the managed
|
before: end.getTime() / 1000,
|
||||||
// range. This is because events may change at any point in time
|
after: start.getTime() / 1000,
|
||||||
// (e.g. a long-running event that ends).
|
|
||||||
before: this._dateEnd.getTime() / 1000,
|
|
||||||
after: this._dateStart.getTime() / 1000,
|
|
||||||
unlimited: true,
|
unlimited: true,
|
||||||
mediaType: mediaType as 'clips' | 'snapshots',
|
mediaType: mediaType as 'clips' | 'snapshots',
|
||||||
});
|
});
|
||||||
@@ -311,7 +404,7 @@ class TimelineEventManager {
|
|||||||
try {
|
try {
|
||||||
results = await multipleBrowseMediaQuery(hass, params);
|
results = await multipleBrowseMediaQuery(hass, params);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(element, (e as Error).message);
|
return dispatchFrigateCardErrorEvent(element, e as FrigateCardError);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [query, result] of results.entries()) {
|
for (const [query, result] of results.entries()) {
|
||||||
@@ -325,7 +418,7 @@ class TimelineEventManager {
|
|||||||
@customElement('frigate-card-timeline')
|
@customElement('frigate-card-timeline')
|
||||||
export class FrigateCardTimeline extends LitElement {
|
export class FrigateCardTimeline extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected hass?: HomeAssistant;
|
protected hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: Readonly<View>;
|
protected view?: Readonly<View>;
|
||||||
@@ -371,7 +464,7 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
@customElement('frigate-card-timeline-core')
|
@customElement('frigate-card-timeline-core')
|
||||||
export class FrigateCardTimelineCore extends LitElement {
|
export class FrigateCardTimelineCore extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected hass?: HomeAssistant;
|
protected hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: Readonly<View>;
|
protected view?: Readonly<View>;
|
||||||
@@ -382,12 +475,17 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected timelineConfig?: TimelineConfig;
|
protected timelineConfig?: TimelineConfig;
|
||||||
|
|
||||||
protected _events = new TimelineEventManager({
|
protected _data = new TimelineDataManager({
|
||||||
tooltipCallback: this._getTooltip.bind(this),
|
tooltipCallback: this._getTooltip.bind(this),
|
||||||
});
|
});
|
||||||
protected _refTimeline: Ref<HTMLElement> = createRef();
|
protected _refTimeline: Ref<HTMLElement> = createRef();
|
||||||
protected _timeline?: Timeline;
|
protected _timeline?: Timeline;
|
||||||
|
|
||||||
|
// Need a way to separate when a user clicks (to pan the timeline) vs when a
|
||||||
|
// user clicks (to choose a recording (non-event) to play). On pan,
|
||||||
|
// _wasDragged will be set to true, and the click subsequently ignored.
|
||||||
|
protected _wasDragged = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a tooltip for a given timeline event.
|
* Get a tooltip for a given timeline event.
|
||||||
* @param source The FrigateBrowseMediaSource in question.
|
* @param source The FrigateBrowseMediaSource in question.
|
||||||
@@ -400,9 +498,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbnailSizeAttr = this.timelineConfig
|
|
||||||
? `thumbnail_size="${this.timelineConfig.controls.thumbnails.size}"`
|
|
||||||
: '';
|
|
||||||
const eventAttr = source.frigate?.event
|
const eventAttr = source.frigate?.event
|
||||||
? `event='${JSON.stringify(source.frigate.event)}'`
|
? `event='${JSON.stringify(source.frigate.event)}'`
|
||||||
: '';
|
: '';
|
||||||
@@ -419,7 +514,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
thumbnail="${source.thumbnail}"
|
thumbnail="${source.thumbnail}"
|
||||||
label="${source.title}"
|
label="${source.title}"
|
||||||
${eventAttr}
|
${eventAttr}
|
||||||
${thumbnailSizeAttr}
|
|
||||||
>
|
>
|
||||||
</frigate-card-thumbnail>`;
|
</frigate-card-thumbnail>`;
|
||||||
}
|
}
|
||||||
@@ -446,12 +540,211 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
></div>`;
|
></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of seconds to seek into a video stream consisting of the
|
||||||
|
* provided segments to reach the target time provided.
|
||||||
|
* @param time Target time.
|
||||||
|
* @param segments A RecordingSegments object.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected _getSeekTime(time: Date, segments: RecordingSegments): number | null {
|
||||||
|
if (!segments.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const target = getUnixTime(time);
|
||||||
|
const hourStart = getUnixTime(startOfHour(time));
|
||||||
|
let seekSeconds = 0;
|
||||||
|
|
||||||
|
// Inspired by: https://github.com/blakeblackshear/frigate/blob/release-0.11.0/web/src/routes/Recording.jsx#L27
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (segment.start_time > target) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const start = segment.start_time < hourStart ? hourStart : segment.start_time;
|
||||||
|
const end = segment.end_time > target ? target : segment.end_time;
|
||||||
|
seekSeconds += end - start;
|
||||||
|
}
|
||||||
|
return seekSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create recording objects.
|
||||||
|
* @param results A map of camera ID to a CameraRecordings object.
|
||||||
|
* @param time The target time for the recordings.
|
||||||
|
* @param onlyMatchingHour If `true` only shows the hour matching the target
|
||||||
|
* for the provided cameras, otherwise shows all hours.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected _createRecordingChildren(
|
||||||
|
results: Map<string, CameraRecordings>,
|
||||||
|
time: Date,
|
||||||
|
onlyMatchingHour: boolean,
|
||||||
|
): FrigateBrowseMediaSource[] {
|
||||||
|
const children: FrigateBrowseMediaSource[] = [];
|
||||||
|
const processedCameras: Set<string> = new Set();
|
||||||
|
|
||||||
|
// Get results in the order the cameras are specified in the configuration.
|
||||||
|
for (const camera of (this.cameras?.keys() || [])) {
|
||||||
|
const recording = results.get(camera);
|
||||||
|
const config = this.cameras?.get(camera);
|
||||||
|
if (!recording || !config?.camera_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a single set of recordings for a given Frigate camera name.
|
||||||
|
// Zones on that same camera do not get separate recordings. The card may
|
||||||
|
// have multiple instances of the same camera for different zoness, so
|
||||||
|
// need to enforce uniqueness here.
|
||||||
|
const uniqueID = `${config.client_id}/${config.camera_name}`;
|
||||||
|
if (processedCameras.has(uniqueID)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processedCameras.add(uniqueID);
|
||||||
|
|
||||||
|
const seekSeconds = this._getSeekTime(time, recording.segments);
|
||||||
|
if (seekSeconds === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dayData of recording.summary) {
|
||||||
|
for (const hourData of dayData.hours) {
|
||||||
|
const hour = add(dayData.day, { hours: hourData.hour });
|
||||||
|
const startHour = startOfHour(hour);
|
||||||
|
const endHour = endOfHour(hour);
|
||||||
|
const isMatchingHour = time >= startHour && time <= endHour;
|
||||||
|
|
||||||
|
if (!onlyMatchingHour || isMatchingHour) {
|
||||||
|
children.push(
|
||||||
|
createVideoChild(
|
||||||
|
`${prettifyTitle(config.camera_name)} ${format(
|
||||||
|
hour,
|
||||||
|
'yyyy-MM-dd HH:mm',
|
||||||
|
)}`,
|
||||||
|
generateRecordingIdentifier({
|
||||||
|
clientId: config.client_id,
|
||||||
|
year: dayData.day.getFullYear(),
|
||||||
|
month: dayData.day.getMonth() + 1,
|
||||||
|
day: dayData.day.getDate(),
|
||||||
|
hour: hourData.hour,
|
||||||
|
cameraName: config.camera_name,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
recording: {
|
||||||
|
camera: config.camera_name,
|
||||||
|
start_time: getUnixTime(startHour),
|
||||||
|
end_time: getUnixTime(endHour),
|
||||||
|
events: hourData.events,
|
||||||
|
...(isMatchingHour && {
|
||||||
|
seek_seconds: seekSeconds,
|
||||||
|
seek_time: time.getTime() / 1000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the view to a recording.
|
||||||
|
* @param time The time of the recording to show.
|
||||||
|
* @param camera An optional camera to show a recording of, otherwise all
|
||||||
|
* cameras are shown at the given time.
|
||||||
|
*/
|
||||||
|
protected async _changeViewToRecording(time: Date, camera?: string): Promise<void> {
|
||||||
|
if (!this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = endOfHour(time);
|
||||||
|
const after = startOfHour(time);
|
||||||
|
const results: Map<string, CameraRecordings> = new Map();
|
||||||
|
|
||||||
|
const fetch = async (camera: string, config?: CameraConfig): Promise<void> => {
|
||||||
|
if (!config || !config.camera_name || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cameraResults = await Promise.all([
|
||||||
|
getRecordingSegments(
|
||||||
|
this.hass,
|
||||||
|
config.client_id,
|
||||||
|
config.camera_name,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
),
|
||||||
|
getRecordingsSummary(this.hass, config.client_id, config.camera_name),
|
||||||
|
]);
|
||||||
|
results.set(camera, { segments: cameraResults[0], summary: cameraResults[1] });
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
const cameras = camera ? [camera] : [...(this.cameras?.keys() ?? [])];
|
||||||
|
await Promise.all(cameras.map((camera) => fetch(camera, this.cameras?.get(camera))));
|
||||||
|
|
||||||
|
const children = this._createRecordingChildren(results, time, !camera);
|
||||||
|
if (!children.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let childIndex = 0;
|
||||||
|
if (camera) {
|
||||||
|
childIndex = children.findIndex(
|
||||||
|
(child) =>
|
||||||
|
child.frigate?.recording &&
|
||||||
|
child.frigate.recording.start_time * 1000 === after.getTime(),
|
||||||
|
);
|
||||||
|
if (childIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.view
|
||||||
|
?.evolve({
|
||||||
|
view: 'media',
|
||||||
|
target: createEventParentForChildren(localize('common.recordings'), children),
|
||||||
|
childIndex: childIndex,
|
||||||
|
})
|
||||||
|
.dispatchChangeEvent(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the range is in the process of being changed.
|
||||||
|
* @param properties
|
||||||
|
*/
|
||||||
|
protected _timelineRangeChangeHandler(
|
||||||
|
properties: TimelineEventPropertiesResult,
|
||||||
|
): void {
|
||||||
|
if (properties.event) {
|
||||||
|
// When a human changes the range, an event will be set.
|
||||||
|
this._wasDragged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the timeline is clicked.
|
||||||
|
* @param properties The properties of the timeline click event.
|
||||||
|
*/
|
||||||
protected _timelineClickHandler(properties: TimelineEventPropertiesResult): void {
|
protected _timelineClickHandler(properties: TimelineEventPropertiesResult): void {
|
||||||
if (properties.what === 'item') {
|
if (properties.what && ['item', 'background'].includes(properties.what)) {
|
||||||
// Prevent interaction with items on the timeline from activating card
|
// Prevent interaction with items on the timeline from activating card
|
||||||
// wide actions.
|
// wide actions.
|
||||||
stopEventFromActivatingCardWideActions(properties.event);
|
stopEventFromActivatingCardWideActions(properties.event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this._wasDragged && properties.what) {
|
||||||
|
if (['background', 'group-label'].includes(properties.what)) {
|
||||||
|
this._changeViewToRecording(properties.time, String(properties.group));
|
||||||
|
} else if (properties.what === 'axis') {
|
||||||
|
this._changeViewToRecording(properties.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._wasDragged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -468,14 +761,15 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.hass && this.cameras && this._timeline && this.timelineConfig) {
|
if (this.hass && this.cameras && this._timeline && this.timelineConfig) {
|
||||||
this._events
|
this._data
|
||||||
.fetchEventsIfNecessary(
|
.fetchIfNecessary(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameras,
|
this.cameras,
|
||||||
this.timelineConfig.media,
|
this.timelineConfig.media,
|
||||||
properties.start,
|
properties.start,
|
||||||
properties.end,
|
properties.end,
|
||||||
|
this.timelineConfig.show_recordings,
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (this._timeline) {
|
if (this._timeline) {
|
||||||
@@ -507,7 +801,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
|
|
||||||
const childIndex = data.items.length
|
const childIndex = data.items.length
|
||||||
? this.view.target.children.findIndex(
|
? this.view.target.children.findIndex(
|
||||||
(child) => child.frigate?.event.id === data.items[0],
|
(child) => child.frigate?.event?.id === data.items[0],
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -560,8 +854,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
const selected = this._timeline.getSelection();
|
const selected = this._timeline.getSelection();
|
||||||
let childIndex = -1;
|
let childIndex = -1;
|
||||||
const children: FrigateBrowseMediaSource[] = [];
|
const children: FrigateBrowseMediaSource[] = [];
|
||||||
this._events.dataset.get({ order: sortEvent }).forEach((item) => {
|
this._data.dataset.get({ order: sortEvent }).forEach((item) => {
|
||||||
if (item.source) {
|
if (item.event && item.source) {
|
||||||
children.push(item.source);
|
children.push(item.source);
|
||||||
if (selected.includes(item.event.id)) {
|
if (selected.includes(item.event.id)) {
|
||||||
childIndex = children.length - 1;
|
childIndex = children.length - 1;
|
||||||
@@ -572,9 +866,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = createEventParentForChildren('Timeline events', children);
|
|
||||||
return {
|
return {
|
||||||
target: target,
|
target: createEventParentForChildren('Timeline events', children),
|
||||||
childIndex: childIndex < 0 ? null : childIndex,
|
childIndex: childIndex < 0 ? null : childIndex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -695,12 +988,14 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
// Never include the target media in a cluster, and never group
|
// Never include the target media in a cluster, and never group
|
||||||
// different object types together (e.g. person and car).
|
// different object types together (e.g. person and car).
|
||||||
return (
|
return (
|
||||||
|
[first.type, second.type].every((type) => type !== 'background') &&
|
||||||
|
first.type === second.type &&
|
||||||
!!first.id &&
|
!!first.id &&
|
||||||
first.id !== this.view?.media?.frigate?.event?.id &&
|
first.id !== this.view?.media?.frigate?.event?.id &&
|
||||||
!!second.id &&
|
!!second.id &&
|
||||||
second.id != this.view?.media?.frigate?.event?.id &&
|
second.id != this.view?.media?.frigate?.event?.id &&
|
||||||
(<FrigateCardTimelineItem>first).event.label ===
|
(<FrigateCardTimelineItem>first).event?.label ===
|
||||||
(<FrigateCardTimelineItem>second).event.label
|
(<FrigateCardTimelineItem>second).event?.label
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -721,13 +1016,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
whiteList: {
|
whiteList: {
|
||||||
'frigate-card-thumbnail': [
|
'frigate-card-thumbnail': ['details', 'thumbnail', 'label', 'event'],
|
||||||
'details',
|
|
||||||
'thumbnail',
|
|
||||||
'label',
|
|
||||||
'event',
|
|
||||||
'thumbnail_size',
|
|
||||||
],
|
|
||||||
div: ['title'],
|
div: ['title'],
|
||||||
span: ['style'],
|
span: ['style'],
|
||||||
},
|
},
|
||||||
@@ -750,13 +1039,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
* Update the timeline from the view object.
|
* Update the timeline from the view object.
|
||||||
*/
|
*/
|
||||||
protected async _updateTimelineFromView(): Promise<void> {
|
protected async _updateTimelineFromView(): Promise<void> {
|
||||||
if (
|
if (!this.hass || !this.cameras || !this.view || !this.timelineConfig) {
|
||||||
!this.hass ||
|
|
||||||
!this.cameras ||
|
|
||||||
!this.view ||
|
|
||||||
!this._timeline ||
|
|
||||||
!this.timelineConfig
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,15 +1048,20 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
? this._getStartEndFromEvent(event)
|
? this._getStartEndFromEvent(event)
|
||||||
: this._getStartEnd();
|
: this._getStartEnd();
|
||||||
|
|
||||||
await this._events.fetchEventsIfNecessary(
|
await this._data.fetchIfNecessary(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameras,
|
this.cameras,
|
||||||
this.timelineConfig.media,
|
this.timelineConfig.media,
|
||||||
windowStart,
|
windowStart,
|
||||||
windowEnd,
|
windowEnd,
|
||||||
|
this.timelineConfig.show_recordings,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!this._timeline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._timeline.setSelection(event ? [event.id] : [], {
|
this._timeline.setSelection(event ? [event.id] : [], {
|
||||||
focus: false,
|
focus: false,
|
||||||
animation: {
|
animation: {
|
||||||
@@ -807,9 +1095,9 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
// Hack: Clustering may not update unless the dataset changes, artifically
|
// Hack: Clustering may not update unless the dataset changes, artifically
|
||||||
// update the dataset to ensure the newly selected item cannot be included
|
// update the dataset to ensure the newly selected item cannot be included
|
||||||
// in a cluster.
|
// in a cluster.
|
||||||
const item = this._events.dataset.get(event.id);
|
const item = this._data.dataset.get(event.id);
|
||||||
if (item) {
|
if (item) {
|
||||||
this._events.dataset.updateOnly(item);
|
this._data.dataset.updateOnly(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -825,7 +1113,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
// -> New view dispatched (to load thumbnails into outer carousel).
|
// -> New view dispatched (to load thumbnails into outer carousel).
|
||||||
// -> New view received ... [loop]
|
// -> New view received ... [loop]
|
||||||
const currentContext = this.view.context as TimelineViewContext | null;
|
const currentContext = this.view.context as TimelineViewContext | null;
|
||||||
if (currentContext?.dateFetch !== this._events.lastFetchDate) {
|
if (!isEqual(currentContext?.dateFetch, this._data.lastFetchDate)) {
|
||||||
const thumbnails = this._generateThumbnails();
|
const thumbnails = this._generateThumbnails();
|
||||||
this.view
|
this.view
|
||||||
?.evolve({
|
?.evolve({
|
||||||
@@ -851,12 +1139,27 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
} else if (currentContext?.window) {
|
} else if (currentContext?.window) {
|
||||||
newContext.window = currentContext.window;
|
newContext.window = currentContext.window;
|
||||||
}
|
}
|
||||||
if (this._events.lastFetchDate) {
|
if (this._data.lastFetchDate) {
|
||||||
newContext.dateFetch = this._events.lastFetchDate;
|
newContext.dateFetch = this._data.lastFetchDate;
|
||||||
}
|
}
|
||||||
return newContext || null;
|
return newContext || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an update will occur.
|
||||||
|
* @param changedProps The changed properties
|
||||||
|
*/
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has('timelineConfig')) {
|
||||||
|
if (this.timelineConfig?.controls.thumbnails.size) {
|
||||||
|
this.style.setProperty(
|
||||||
|
'--frigate-card-thumbnail-size',
|
||||||
|
`${this.timelineConfig.controls.thumbnails.size}px`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the component is updated.
|
* Called when the component is updated.
|
||||||
* @param changedProperties The changed properties if any.
|
* @param changedProperties The changed properties if any.
|
||||||
@@ -865,7 +1168,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (changedProperties.has('cameras')) {
|
if (changedProperties.has('cameras')) {
|
||||||
this._events.clear();
|
this._data.clear();
|
||||||
this._timeline?.destroy();
|
this._timeline?.destroy();
|
||||||
this._timeline = undefined;
|
this._timeline = undefined;
|
||||||
}
|
}
|
||||||
@@ -888,14 +1191,14 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
|
|
||||||
this._timeline = new Timeline(
|
this._timeline = new Timeline(
|
||||||
this._refTimeline.value,
|
this._refTimeline.value,
|
||||||
this._events.dataset,
|
this._data.dataset,
|
||||||
groups,
|
groups,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
this._timeline.on('select', this._timelineSelectHandler.bind(this));
|
this._timeline.on('select', this._timelineSelectHandler.bind(this));
|
||||||
this._timeline.on('rangechanged', this._timelineRangeHandler.bind(this));
|
this._timeline.on('rangechanged', this._timelineRangeHandler.bind(this));
|
||||||
this._timeline.on('click', this._timelineClickHandler.bind(this));
|
this._timeline.on('click', this._timelineClickHandler.bind(this));
|
||||||
this._timeline.on('doubleclick', this._timelineClickHandler.bind(this));
|
this._timeline.on('rangechange', this._timelineRangeChangeHandler.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
-10
@@ -22,6 +22,7 @@ import type {
|
|||||||
CameraConfig,
|
CameraConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
|
FrigateCardMediaPlayer,
|
||||||
MediaShowInfo,
|
MediaShowInfo,
|
||||||
TransitionEffect,
|
TransitionEffect,
|
||||||
ViewerConfig
|
ViewerConfig
|
||||||
@@ -36,8 +37,8 @@ import {
|
|||||||
multipleBrowseMediaQueryMerged,
|
multipleBrowseMediaQueryMerged,
|
||||||
overrideMultiBrowseMediaQueryParameters
|
overrideMultiBrowseMediaQueryParameters
|
||||||
} from '../utils/ha/browse-media.js';
|
} from '../utils/ha/browse-media.js';
|
||||||
import { createMediaShowInfo } from '../utils/media-info.js';
|
|
||||||
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||||
|
import { createMediaShowInfo } from '../utils/media-info.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||||
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
|
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
|
||||||
@@ -125,6 +126,8 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FRIGATE_CARD_HLS_SELECTOR = 'frigate-card-ha-hls-player';
|
||||||
|
|
||||||
@customElement('frigate-card-viewer-carousel')
|
@customElement('frigate-card-viewer-carousel')
|
||||||
export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -168,11 +171,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
++i
|
++i
|
||||||
) {
|
) {
|
||||||
if (isTrueMedia(target.children[i])) {
|
if (isTrueMedia(target.children[i])) {
|
||||||
await resolveMedia(
|
await resolveMedia(this.hass, target.children[i], this.resolvedMediaCache);
|
||||||
this.hass,
|
|
||||||
target.children[i],
|
|
||||||
this.resolvedMediaCache,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -278,6 +277,23 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The the HLS player on a slide (or current slide if not provided.)
|
||||||
|
* @param slide An optional slide.
|
||||||
|
* @returns The FrigateCardMediaPlayer or null if not found.
|
||||||
|
*/
|
||||||
|
protected _getPlayer(slide?: HTMLElement): FrigateCardMediaPlayer | null {
|
||||||
|
if (this._carousel) {
|
||||||
|
if (!slide) {
|
||||||
|
slide = this._carousel.slideNodes()[this._carousel.selectedScrollSnap()];
|
||||||
|
}
|
||||||
|
return slide?.querySelector(
|
||||||
|
FRIGATE_CARD_HLS_SELECTOR,
|
||||||
|
) as FrigateCardMediaPlayer | null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Embla plugins to use.
|
* Get the Embla plugins to use.
|
||||||
* @returns An EmblaOptionsType object or undefined for no options.
|
* @returns An EmblaOptionsType object or undefined for no options.
|
||||||
@@ -291,7 +307,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
AutoMediaPlugin({
|
AutoMediaPlugin({
|
||||||
playerSelector: 'frigate-card-ha-hls-player',
|
playerSelector: FRIGATE_CARD_HLS_SELECTOR,
|
||||||
...(this.viewerConfig?.auto_play && {
|
...(this.viewerConfig?.auto_play && {
|
||||||
autoPlayCondition: this.viewerConfig.auto_play,
|
autoPlayCondition: this.viewerConfig.auto_play,
|
||||||
}),
|
}),
|
||||||
@@ -511,9 +527,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
const img = slide.querySelector('img') as HTMLImageElement;
|
const img = slide.querySelector('img') as HTMLImageElement;
|
||||||
|
|
||||||
// Frigate >= 0.9.0+ clips.
|
// Frigate >= 0.9.0+ clips.
|
||||||
const hls_player = slide.querySelector(
|
const hls_player = this._getPlayer(slide) as FrigateCardMediaPlayer & {
|
||||||
'frigate-card-ha-hls-player',
|
url: string;
|
||||||
) as HTMLElement & { url: string };
|
};
|
||||||
|
|
||||||
if (img) {
|
if (img) {
|
||||||
img.src = this._canonicalizeHAURL(resolvedMedia.url) || '';
|
img.src = this._canonicalizeHAURL(resolvedMedia.url) || '';
|
||||||
@@ -661,6 +677,24 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
: ``} `;
|
: ``} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire a media show event when a slide is selected.
|
||||||
|
*/
|
||||||
|
protected _selectSlideMediaShowHandler(): void {
|
||||||
|
super._selectSlideMediaShowHandler();
|
||||||
|
|
||||||
|
// If this is a recording and play is desired to be started from a
|
||||||
|
// particular point, seek to that point. Use the media off the slide itself
|
||||||
|
// -- when the slide is changed, the media show event may be dispatched
|
||||||
|
// before this.view has been updated to reflect the new selection.
|
||||||
|
const player = this._getPlayer() as FrigateCardMediaPlayer & {
|
||||||
|
media?: FrigateBrowseMediaSource;
|
||||||
|
};
|
||||||
|
if (player && player.media && player.media.frigate?.recording?.seek_seconds) {
|
||||||
|
player.seek(player.media.frigate.recording.seek_seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected _renderMediaItem(
|
protected _renderMediaItem(
|
||||||
mediaToRender: FrigateBrowseMediaSource,
|
mediaToRender: FrigateBrowseMediaSource,
|
||||||
slideIndex: number,
|
slideIndex: number,
|
||||||
@@ -682,6 +716,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The media is attached to the player as '.media' which is used in
|
||||||
|
// `_selectSlideMediaShowHandler` (and not used by the player itself).
|
||||||
return html`
|
return html`
|
||||||
<div class="embla__slide">
|
<div class="embla__slide">
|
||||||
${mediaToRender.media_content_type === 'video'
|
${mediaToRender.media_content_type === 'video'
|
||||||
@@ -696,6 +732,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
url=${ifDefined(
|
url=${ifDefined(
|
||||||
lazyLoad ? undefined : this._canonicalizeHAURL(resolvedMedia?.url),
|
lazyLoad ? undefined : this._canonicalizeHAURL(resolvedMedia?.url),
|
||||||
)}
|
)}
|
||||||
|
.media=${mediaToRender}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
||||||
this._mediaShowEventHandler(slideIndex, e)}
|
this._mediaShowEventHandler(slideIndex, e)}
|
||||||
|
|||||||
+16
-19
@@ -8,42 +8,38 @@ import {
|
|||||||
CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
|
CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
|
||||||
CONF_CAMERAS_ARRAY_URL,
|
CONF_CAMERAS_ARRAY_URL,
|
||||||
CONF_CAMERAS_ARRAY_ZONE,
|
CONF_CAMERAS_ARRAY_ZONE,
|
||||||
CONF_EVENT_VIEWER_AUTO_PLAY,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
|
||||||
CONF_IMAGE_URL,
|
CONF_IMAGE_URL,
|
||||||
|
CONF_LIVE_AUTO_UNMUTE,
|
||||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
|
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
CONF_LIVE_LAZY_UNLOAD,
|
CONF_LIVE_LAZY_UNLOAD,
|
||||||
CONF_LIVE_PRELOAD,
|
CONF_LIVE_PRELOAD,
|
||||||
CONF_LIVE_WEBRTC_CARD,
|
CONF_LIVE_WEBRTC_CARD,
|
||||||
|
CONF_MEDIA_VIEWER,
|
||||||
CONF_MENU,
|
CONF_MENU,
|
||||||
CONF_MENU_BUTTONS_FRIGATE,
|
|
||||||
CONF_MENU_BUTTONS_CAMERAS,
|
CONF_MENU_BUTTONS_CAMERAS,
|
||||||
CONF_MENU_BUTTONS_LIVE,
|
|
||||||
CONF_MENU_BUTTONS_CLIPS,
|
CONF_MENU_BUTTONS_CLIPS,
|
||||||
CONF_MENU_BUTTONS_SNAPSHOTS,
|
|
||||||
CONF_MENU_BUTTONS_IMAGE,
|
|
||||||
CONF_MENU_BUTTONS_DOWNLOAD,
|
CONF_MENU_BUTTONS_DOWNLOAD,
|
||||||
|
CONF_MENU_BUTTONS_FRIGATE,
|
||||||
CONF_MENU_BUTTONS_FRIGATE_UI,
|
CONF_MENU_BUTTONS_FRIGATE_UI,
|
||||||
CONF_MENU_BUTTONS_FULLSCREEN,
|
CONF_MENU_BUTTONS_FULLSCREEN,
|
||||||
|
CONF_MENU_BUTTONS_IMAGE,
|
||||||
|
CONF_MENU_BUTTONS_LIVE,
|
||||||
|
CONF_MENU_BUTTONS_SNAPSHOTS,
|
||||||
CONF_MENU_BUTTON_SIZE,
|
CONF_MENU_BUTTON_SIZE,
|
||||||
CONF_MENU_POSITION,
|
CONF_MENU_POSITION,
|
||||||
CONF_MENU_STYLE,
|
CONF_MENU_STYLE,
|
||||||
CONF_OVERRIDES,
|
CONF_OVERRIDES,
|
||||||
CONF_VIEW_DEFAULT,
|
CONF_VIEW_DEFAULT,
|
||||||
CONF_VIEW_TIMEOUT_SECONDS,
|
CONF_VIEW_TIMEOUT_SECONDS,
|
||||||
CONF_VIEW_UPDATE_ENTITIES,
|
CONF_VIEW_UPDATE_ENTITIES
|
||||||
CONF_LIVE_AUTO_UNMUTE,
|
|
||||||
CONF_EVENT_VIEWER_AUTO_UNMUTE,
|
|
||||||
} from './const';
|
} from './const';
|
||||||
import {
|
import {
|
||||||
BUTTON_SIZE_MIN,
|
BUTTON_SIZE_MIN,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
RawFrigateCardConfigArray,
|
RawFrigateCardConfigArray,
|
||||||
THUMBNAIL_WIDTH_MAX,
|
THUMBNAIL_WIDTH_MAX,
|
||||||
THUMBNAIL_WIDTH_MIN,
|
THUMBNAIL_WIDTH_MIN
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -532,8 +528,8 @@ const UPGRADES = [
|
|||||||
upgradeMoveTo('live_preload', CONF_LIVE_PRELOAD),
|
upgradeMoveTo('live_preload', CONF_LIVE_PRELOAD),
|
||||||
upgradeMoveTo('webrtc', 'live.webrtc'),
|
upgradeMoveTo('webrtc', 'live.webrtc'),
|
||||||
upgradeMoveTo('autoplay_clip', 'event_viewer.autoplay_clip'),
|
upgradeMoveTo('autoplay_clip', 'event_viewer.autoplay_clip'),
|
||||||
upgradeMoveTo('controls.nextprev', CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE),
|
upgradeMoveTo('controls.nextprev', 'event_viewer.controls.next_previous.style'),
|
||||||
upgradeMoveTo('controls.nextprev_size', CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE),
|
upgradeMoveTo('controls.nextprev_size', 'event_viewer.controls.next_previous.size'),
|
||||||
upgradeMoveTo('menu_mode', 'menu.mode'),
|
upgradeMoveTo('menu_mode', 'menu.mode'),
|
||||||
upgradeMoveTo('menu_buttons', 'menu.buttons'),
|
upgradeMoveTo('menu_buttons', 'menu.buttons'),
|
||||||
upgradeMoveTo('menu_button_size', CONF_MENU_BUTTON_SIZE),
|
upgradeMoveTo('menu_button_size', CONF_MENU_BUTTON_SIZE),
|
||||||
@@ -546,7 +542,7 @@ const UPGRADES = [
|
|||||||
upgradeToMultipleCameras(),
|
upgradeToMultipleCameras(),
|
||||||
upgradeMenuConditionToMenuOverride(),
|
upgradeMenuConditionToMenuOverride(),
|
||||||
upgradeMoveTo('view.timeout', CONF_VIEW_TIMEOUT_SECONDS, toNumberOrIgnore),
|
upgradeMoveTo('view.timeout', CONF_VIEW_TIMEOUT_SECONDS, toNumberOrIgnore),
|
||||||
upgradeMoveTo('event_viewer.autoplay_clip', CONF_EVENT_VIEWER_AUTO_PLAY),
|
upgradeMoveTo('event_viewer.autoplay_clip', 'event_viewer.auto_play'),
|
||||||
|
|
||||||
// v3.0.0-rc.1 -> v3.0.0-rc.2
|
// v3.0.0-rc.1 -> v3.0.0-rc.2
|
||||||
upgradeArrayValue(
|
upgradeArrayValue(
|
||||||
@@ -565,7 +561,7 @@ const UPGRADES = [
|
|||||||
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
|
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
|
||||||
),
|
),
|
||||||
upgradeWithOverrides(
|
upgradeWithOverrides(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
'event_viewer.controls.thumbnails.size',
|
||||||
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
|
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
|
||||||
),
|
),
|
||||||
upgradeWithOverrides(
|
upgradeWithOverrides(
|
||||||
@@ -573,7 +569,7 @@ const UPGRADES = [
|
|||||||
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
|
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
|
||||||
),
|
),
|
||||||
upgradeWithOverrides(
|
upgradeWithOverrides(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
'event_viewer.controls.next_previous.size',
|
||||||
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
|
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
|
||||||
),
|
),
|
||||||
upgradeWithOverrides(
|
upgradeWithOverrides(
|
||||||
@@ -597,10 +593,11 @@ const UPGRADES = [
|
|||||||
upgrade(CONF_LIVE_AUTO_UNMUTE, (val) =>
|
upgrade(CONF_LIVE_AUTO_UNMUTE, (val) =>
|
||||||
typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined,
|
typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined,
|
||||||
),
|
),
|
||||||
upgrade(CONF_EVENT_VIEWER_AUTO_PLAY, (val) =>
|
upgrade('event_viewer.auto_play', (val) =>
|
||||||
typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined,
|
typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined,
|
||||||
),
|
),
|
||||||
upgrade(CONF_EVENT_VIEWER_AUTO_UNMUTE, (val) =>
|
upgrade('event_viewer.auto_unmute', (val) =>
|
||||||
typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined,
|
typeof val === 'boolean' ? (val ? 'all' : 'never') : undefined,
|
||||||
),
|
),
|
||||||
|
upgradeMoveToWithOverrides('event_viewer', CONF_MEDIA_VIEWER),
|
||||||
];
|
];
|
||||||
|
|||||||
+26
-25
@@ -52,31 +52,31 @@ export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
|
|||||||
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE =
|
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE =
|
||||||
`${CONF_EVENT_GALLERY}.controls.thumbnails.size` as const;
|
`${CONF_EVENT_GALLERY}.controls.thumbnails.size` as const;
|
||||||
|
|
||||||
export const CONF_EVENT_VIEWER = 'event_viewer' as const;
|
export const CONF_MEDIA_VIEWER = 'media_viewer' as const;
|
||||||
export const CONF_EVENT_VIEWER_AUTO_PLAY = `${CONF_EVENT_VIEWER}.auto_play` as const;
|
export const CONF_MEDIA_VIEWER_AUTO_PLAY = `${CONF_MEDIA_VIEWER}.auto_play` as const;
|
||||||
export const CONF_EVENT_VIEWER_AUTO_PAUSE = `${CONF_EVENT_VIEWER}.auto_pause` as const;
|
export const CONF_MEDIA_VIEWER_AUTO_PAUSE = `${CONF_MEDIA_VIEWER}.auto_pause` as const;
|
||||||
export const CONF_EVENT_VIEWER_AUTO_MUTE = `${CONF_EVENT_VIEWER}.auto_mute` as const;
|
export const CONF_MEDIA_VIEWER_AUTO_MUTE = `${CONF_MEDIA_VIEWER}.auto_mute` as const;
|
||||||
export const CONF_EVENT_VIEWER_AUTO_UNMUTE = `${CONF_EVENT_VIEWER}.auto_unmute` as const;
|
export const CONF_MEDIA_VIEWER_AUTO_UNMUTE = `${CONF_MEDIA_VIEWER}.auto_unmute` as const;
|
||||||
export const CONF_EVENT_VIEWER_DRAGGABLE = `${CONF_EVENT_VIEWER}.draggable` as const;
|
export const CONF_MEDIA_VIEWER_DRAGGABLE = `${CONF_MEDIA_VIEWER}.draggable` as const;
|
||||||
export const CONF_EVENT_VIEWER_LAZY_LOAD = `${CONF_EVENT_VIEWER}.lazy_load` as const;
|
export const CONF_MEDIA_VIEWER_LAZY_LOAD = `${CONF_MEDIA_VIEWER}.lazy_load` as const;
|
||||||
export const CONF_EVENT_VIEWER_TRANSITION_EFFECT =
|
export const CONF_MEDIA_VIEWER_TRANSITION_EFFECT =
|
||||||
`${CONF_EVENT_VIEWER}.transition_effect` as const;
|
`${CONF_MEDIA_VIEWER}.transition_effect` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE =
|
export const CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE =
|
||||||
`${CONF_EVENT_VIEWER}.controls.next_previous.style` as const;
|
`${CONF_MEDIA_VIEWER}.controls.next_previous.style` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
|
export const CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
|
||||||
`${CONF_EVENT_VIEWER}.controls.next_previous.size` as const;
|
`${CONF_MEDIA_VIEWER}.controls.next_previous.size` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE =
|
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE =
|
||||||
`${CONF_EVENT_VIEWER}.controls.thumbnails.mode` as const;
|
`${CONF_MEDIA_VIEWER}.controls.thumbnails.mode` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS =
|
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS =
|
||||||
`${CONF_EVENT_VIEWER}.controls.thumbnails.show_details` as const;
|
`${CONF_MEDIA_VIEWER}.controls.thumbnails.show_details` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
|
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
|
||||||
`${CONF_EVENT_VIEWER}.controls.thumbnails.show_controls` as const;
|
`${CONF_MEDIA_VIEWER}.controls.thumbnails.show_controls` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE =
|
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE =
|
||||||
`${CONF_EVENT_VIEWER}.controls.thumbnails.size` as const;
|
`${CONF_MEDIA_VIEWER}.controls.thumbnails.size` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_TITLE_MODE =
|
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE =
|
||||||
`${CONF_EVENT_VIEWER}.controls.title.mode` as const;
|
`${CONF_MEDIA_VIEWER}.controls.title.mode` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_TITLE_DURATION_SECONDS =
|
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS =
|
||||||
`${CONF_EVENT_VIEWER}.controls.title.duration_seconds` as const;
|
`${CONF_MEDIA_VIEWER}.controls.title.duration_seconds` as const;
|
||||||
|
|
||||||
export const CONF_LIVE = 'live' as const;
|
export const CONF_LIVE = 'live' as const;
|
||||||
export const CONF_LIVE_AUTO_PLAY = `${CONF_LIVE}.auto_play` as const;
|
export const CONF_LIVE_AUTO_PLAY = `${CONF_LIVE}.auto_play` as const;
|
||||||
@@ -118,6 +118,7 @@ export const CONF_TIMELINE_WINDOW_SECONDS = `${CONF_TIMELINE}.window_seconds` as
|
|||||||
export const CONF_TIMELINE_CLUSTERING_THRESHOLD =
|
export const CONF_TIMELINE_CLUSTERING_THRESHOLD =
|
||||||
`${CONF_TIMELINE}.clustering_threshold` as const;
|
`${CONF_TIMELINE}.clustering_threshold` as const;
|
||||||
export const CONF_TIMELINE_MEDIA = `${CONF_TIMELINE}.media` as const;
|
export const CONF_TIMELINE_MEDIA = `${CONF_TIMELINE}.media` as const;
|
||||||
|
export const CONF_TIMELINE_SHOW_RECORDINGS = `${CONF_TIMELINE}.show_recordings` as const;
|
||||||
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE =
|
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE =
|
||||||
`${CONF_TIMELINE}.controls.thumbnails.mode` as const;
|
`${CONF_TIMELINE}.controls.thumbnails.mode` as const;
|
||||||
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE =
|
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE =
|
||||||
|
|||||||
+62
-60
@@ -32,23 +32,7 @@ import {
|
|||||||
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
|
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
|
||||||
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
||||||
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE,
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE, CONF_IMAGE_MODE,
|
||||||
CONF_EVENT_VIEWER_AUTO_MUTE,
|
|
||||||
CONF_EVENT_VIEWER_AUTO_PAUSE,
|
|
||||||
CONF_EVENT_VIEWER_AUTO_PLAY,
|
|
||||||
CONF_EVENT_VIEWER_AUTO_UNMUTE,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
|
|
||||||
CONF_EVENT_VIEWER_CONTROLS_TITLE_MODE,
|
|
||||||
CONF_EVENT_VIEWER_DRAGGABLE,
|
|
||||||
CONF_EVENT_VIEWER_LAZY_LOAD,
|
|
||||||
CONF_EVENT_VIEWER_TRANSITION_EFFECT,
|
|
||||||
CONF_IMAGE_MODE,
|
|
||||||
CONF_IMAGE_REFRESH_SECONDS,
|
CONF_IMAGE_REFRESH_SECONDS,
|
||||||
CONF_IMAGE_URL,
|
CONF_IMAGE_URL,
|
||||||
CONF_LIVE_AUTO_MUTE,
|
CONF_LIVE_AUTO_MUTE,
|
||||||
@@ -68,8 +52,21 @@ import {
|
|||||||
CONF_LIVE_LAZY_LOAD,
|
CONF_LIVE_LAZY_LOAD,
|
||||||
CONF_LIVE_LAZY_UNLOAD,
|
CONF_LIVE_LAZY_UNLOAD,
|
||||||
CONF_LIVE_PRELOAD,
|
CONF_LIVE_PRELOAD,
|
||||||
CONF_LIVE_TRANSITION_EFFECT,
|
CONF_LIVE_TRANSITION_EFFECT, CONF_MEDIA_VIEWER_AUTO_MUTE,
|
||||||
CONF_MENU_ALIGNMENT,
|
CONF_MEDIA_VIEWER_AUTO_PAUSE,
|
||||||
|
CONF_MEDIA_VIEWER_AUTO_PLAY,
|
||||||
|
CONF_MEDIA_VIEWER_AUTO_UNMUTE,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
|
||||||
|
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
|
||||||
|
CONF_MEDIA_VIEWER_DRAGGABLE,
|
||||||
|
CONF_MEDIA_VIEWER_LAZY_LOAD,
|
||||||
|
CONF_MEDIA_VIEWER_TRANSITION_EFFECT, CONF_MENU_ALIGNMENT,
|
||||||
CONF_MENU_BUTTONS,
|
CONF_MENU_BUTTONS,
|
||||||
CONF_MENU_BUTTON_SIZE,
|
CONF_MENU_BUTTON_SIZE,
|
||||||
CONF_MENU_POSITION,
|
CONF_MENU_POSITION,
|
||||||
@@ -80,6 +77,7 @@ import {
|
|||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
CONF_TIMELINE_MEDIA,
|
CONF_TIMELINE_MEDIA,
|
||||||
|
CONF_TIMELINE_SHOW_RECORDINGS,
|
||||||
CONF_TIMELINE_WINDOW_SECONDS,
|
CONF_TIMELINE_WINDOW_SECONDS,
|
||||||
CONF_VIEW_CAMERA_SELECT,
|
CONF_VIEW_CAMERA_SELECT,
|
||||||
CONF_VIEW_DARK_MODE,
|
CONF_VIEW_DARK_MODE,
|
||||||
@@ -152,10 +150,10 @@ const options: EditorOptions = {
|
|||||||
name: localize('editor.live'),
|
name: localize('editor.live'),
|
||||||
secondary: localize('editor.live_secondary'),
|
secondary: localize('editor.live_secondary'),
|
||||||
},
|
},
|
||||||
event_viewer: {
|
media_viewer: {
|
||||||
icon: 'filmstrip',
|
icon: 'filmstrip',
|
||||||
name: localize('editor.event_viewer'),
|
name: localize('editor.media_viewer'),
|
||||||
secondary: localize('editor.event_viewer_secondary'),
|
secondary: localize('editor.media_viewer_secondary'),
|
||||||
},
|
},
|
||||||
event_gallery: {
|
event_gallery: {
|
||||||
icon: 'grid',
|
icon: 'grid',
|
||||||
@@ -239,15 +237,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{
|
{
|
||||||
value: 'thumbnails',
|
value: 'thumbnails',
|
||||||
label: localize('config.event_viewer.controls.next_previous.styles.thumbnails'),
|
label: localize('config.media_viewer.controls.next_previous.styles.thumbnails'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'chevrons',
|
value: 'chevrons',
|
||||||
label: localize('config.event_viewer.controls.next_previous.styles.chevrons'),
|
label: localize('config.media_viewer.controls.next_previous.styles.chevrons'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'none',
|
value: 'none',
|
||||||
label: localize('config.event_viewer.controls.next_previous.styles.none'),
|
label: localize('config.media_viewer.controls.next_previous.styles.none'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -281,23 +279,23 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{
|
{
|
||||||
value: 'none',
|
value: 'none',
|
||||||
label: localize('config.event_viewer.controls.thumbnails.modes.none'),
|
label: localize('config.media_viewer.controls.thumbnails.modes.none'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'above',
|
value: 'above',
|
||||||
label: localize('config.event_viewer.controls.thumbnails.modes.above'),
|
label: localize('config.media_viewer.controls.thumbnails.modes.above'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'below',
|
value: 'below',
|
||||||
label: localize('config.event_viewer.controls.thumbnails.modes.below'),
|
label: localize('config.media_viewer.controls.thumbnails.modes.below'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'left',
|
value: 'left',
|
||||||
label: localize('config.event_viewer.controls.thumbnails.modes.left'),
|
label: localize('config.media_viewer.controls.thumbnails.modes.left'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'right',
|
value: 'right',
|
||||||
label: localize('config.event_viewer.controls.thumbnails.modes.right'),
|
label: localize('config.media_viewer.controls.thumbnails.modes.right'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -312,29 +310,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
|
|
||||||
protected _titleModes: EditorSelectOption[] = [
|
protected _titleModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'none', label: localize('config.event_viewer.controls.title.modes.none') },
|
{ value: 'none', label: localize('config.media_viewer.controls.title.modes.none') },
|
||||||
{
|
{
|
||||||
value: 'popup-top-left',
|
value: 'popup-top-left',
|
||||||
label: localize('config.event_viewer.controls.title.modes.popup-top-left'),
|
label: localize('config.media_viewer.controls.title.modes.popup-top-left'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'popup-top-right',
|
value: 'popup-top-right',
|
||||||
label: localize('config.event_viewer.controls.title.modes.popup-top-right'),
|
label: localize('config.media_viewer.controls.title.modes.popup-top-right'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'popup-bottom-left',
|
value: 'popup-bottom-left',
|
||||||
label: localize('config.event_viewer.controls.title.modes.popup-bottom-left'),
|
label: localize('config.media_viewer.controls.title.modes.popup-bottom-left'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'popup-bottom-right',
|
value: 'popup-bottom-right',
|
||||||
label: localize('config.event_viewer.controls.title.modes.popup-bottom-right'),
|
label: localize('config.media_viewer.controls.title.modes.popup-bottom-right'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _transitionEffects: EditorSelectOption[] = [
|
protected _transitionEffects: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'none', label: localize('config.event_viewer.transition_effects.none') },
|
{ value: 'none', label: localize('config.media_viewer.transition_effects.none') },
|
||||||
{ value: 'slide', label: localize('config.event_viewer.transition_effects.slide') },
|
{ value: 'slide', label: localize('config.media_viewer.transition_effects.slide') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _imageModes: EditorSelectOption[] = [
|
protected _imageModes: EditorSelectOption[] = [
|
||||||
@@ -1134,74 +1132,74 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
})}
|
})}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
defaults.event_viewer.controls.thumbnails.show_details,
|
defaults.media_viewer.controls.thumbnails.show_details,
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
||||||
defaults.event_viewer.controls.thumbnails.show_controls,
|
defaults.media_viewer.controls.thumbnails.show_controls,
|
||||||
)}
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
: ''}
|
: ''}
|
||||||
${this._renderOptionSetHeader('event_viewer')}
|
${this._renderOptionSetHeader('media_viewer')}
|
||||||
${this._expandedMenus[MENU_OPTIONS] === 'event_viewer'
|
${this._expandedMenus[MENU_OPTIONS] === 'media_viewer'
|
||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_AUTO_PLAY,
|
CONF_MEDIA_VIEWER_AUTO_PLAY,
|
||||||
this._mediaActionPositiveConditions,
|
this._mediaActionPositiveConditions,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_AUTO_PAUSE,
|
CONF_MEDIA_VIEWER_AUTO_PAUSE,
|
||||||
this._mediaActionNegativeConditions,
|
this._mediaActionNegativeConditions,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_AUTO_MUTE,
|
CONF_MEDIA_VIEWER_AUTO_MUTE,
|
||||||
this._mediaActionNegativeConditions,
|
this._mediaActionNegativeConditions,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_AUTO_UNMUTE,
|
CONF_MEDIA_VIEWER_AUTO_UNMUTE,
|
||||||
this._mediaActionPositiveConditions,
|
this._mediaActionPositiveConditions,
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_EVENT_VIEWER_DRAGGABLE,
|
CONF_MEDIA_VIEWER_DRAGGABLE,
|
||||||
defaults.event_viewer.draggable,
|
defaults.media_viewer.draggable,
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_EVENT_VIEWER_LAZY_LOAD,
|
CONF_MEDIA_VIEWER_LAZY_LOAD,
|
||||||
defaults.event_viewer.lazy_load,
|
defaults.media_viewer.lazy_load,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
this._eventViewerNextPreviousControlStyles,
|
this._eventViewerNextPreviousControlStyles,
|
||||||
)}
|
)}
|
||||||
${this._renderNumberInput(CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, {
|
${this._renderNumberInput(CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, {
|
||||||
min: BUTTON_SIZE_MIN,
|
min: BUTTON_SIZE_MIN,
|
||||||
})}
|
})}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||||
this._thumbnailModes,
|
this._thumbnailModes,
|
||||||
)}
|
)}
|
||||||
${this._renderNumberInput(CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, {
|
${this._renderNumberInput(CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE, {
|
||||||
min: THUMBNAIL_WIDTH_MIN,
|
min: THUMBNAIL_WIDTH_MIN,
|
||||||
max: THUMBNAIL_WIDTH_MAX,
|
max: THUMBNAIL_WIDTH_MAX,
|
||||||
})}
|
})}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
defaults.event_viewer.controls.thumbnails.show_details,
|
defaults.media_viewer.controls.thumbnails.show_details,
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
||||||
defaults.event_viewer.controls.thumbnails.show_controls,
|
defaults.media_viewer.controls.thumbnails.show_controls,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_TITLE_MODE,
|
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
|
||||||
this._titleModes,
|
this._titleModes,
|
||||||
)}
|
)}
|
||||||
${this._renderNumberInput(
|
${this._renderNumberInput(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
|
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
|
||||||
{ min: 0, max: 60 },
|
{ min: 0, max: 60 },
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_TRANSITION_EFFECT,
|
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||||
this._transitionEffects,
|
this._transitionEffects,
|
||||||
)}
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
@@ -1223,6 +1221,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_TIMELINE_MEDIA,
|
CONF_TIMELINE_MEDIA,
|
||||||
this._timelineMediaTypes,
|
this._timelineMediaTypes,
|
||||||
)}
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_TIMELINE_SHOW_RECORDINGS,
|
||||||
|
defaults.timeline.show_recordings,
|
||||||
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
||||||
this._thumbnailModes,
|
this._thumbnailModes,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"no_clips": "No clips",
|
"no_clips": "No clips",
|
||||||
"no_snapshot": "No recent snapshot",
|
"no_snapshot": "No recent snapshot",
|
||||||
"no_clip": "No recent clip",
|
"no_clip": "No recent clip",
|
||||||
"live": "Live"
|
"live": "Live",
|
||||||
|
"recordings": "Recordings"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"cameras": {
|
"cameras": {
|
||||||
@@ -84,22 +85,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"event_viewer": {
|
"media_viewer": {
|
||||||
"auto_play": "Automatically play media",
|
"auto_play": "Automatically play media",
|
||||||
"auto_pause": "Automatically pause media",
|
"auto_pause": "Automatically pause media",
|
||||||
"auto_mute": "Automatically mute media",
|
"auto_mute": "Automatically mute media",
|
||||||
"auto_unmute": "Automatically unmute media",
|
"auto_unmute": "Automatically unmute media",
|
||||||
"draggable": "Event Viewer can be dragged/swiped",
|
"draggable": "Media Viewer can be dragged/swiped",
|
||||||
"lazy_load": "Event Viewer media is lazily loaded in carousel",
|
"lazy_load": "Media Viewer media is lazily loaded in carousel",
|
||||||
"transition_effect": "Event Viewer transition effect",
|
"transition_effect": "Media Viewer transition effect",
|
||||||
"transition_effects": {
|
"transition_effects": {
|
||||||
"none": "No transition",
|
"none": "No transition",
|
||||||
"slide": "Slide transition"
|
"slide": "Slide transition"
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
"next_previous": {
|
"next_previous": {
|
||||||
"style": "Event Viewer next & previous control style",
|
"style": "Media Viewer next & previous control style",
|
||||||
"size": "Event Viewer next & previous control size in pixels",
|
"size": "Media Viewer next & previous control size in pixels",
|
||||||
"styles": {
|
"styles": {
|
||||||
"thumbnails": "Thumbnails",
|
"thumbnails": "Thumbnails",
|
||||||
"chevrons": "Chevrons",
|
"chevrons": "Chevrons",
|
||||||
@@ -107,10 +108,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"thumbnails": {
|
"thumbnails": {
|
||||||
"mode": "Event Viewer thumbnails mode",
|
"mode": "Media Viewer thumbnails mode",
|
||||||
"size": "Event Viewer thumbnails size in pixels",
|
"size": "Media Viewer thumbnails size in pixels",
|
||||||
"show_details": "Show event details with thumbnails",
|
"show_details": "Show details with thumbnails",
|
||||||
"show_controls": "Show event controls with thumbnails",
|
"show_controls": "Show controls with thumbnails",
|
||||||
"modes": {
|
"modes": {
|
||||||
"below": "Thumbnails below the media",
|
"below": "Thumbnails below the media",
|
||||||
"above": "Thumbnails above the media",
|
"above": "Thumbnails above the media",
|
||||||
@@ -120,7 +121,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"mode": "Event Viewer media title display mode",
|
"mode": "Media Viewer media title display mode",
|
||||||
"modes": {
|
"modes": {
|
||||||
"none": "No title display",
|
"none": "No title display",
|
||||||
"popup-top-left": "Popup on the top left",
|
"popup-top-left": "Popup on the top left",
|
||||||
@@ -229,6 +230,7 @@
|
|||||||
"window_seconds": "The default length of the timeline view in seconds",
|
"window_seconds": "The default length of the timeline view in seconds",
|
||||||
"clustering_threshold": "The count of events at which they are clustered (0=no clustering)",
|
"clustering_threshold": "The count of events at which they are clustered (0=no clustering)",
|
||||||
"media": "The media the timeline displays",
|
"media": "The media the timeline displays",
|
||||||
|
"show_recordings": "Show recordings",
|
||||||
"medias": {
|
"medias": {
|
||||||
"all": "All media types",
|
"all": "All media types",
|
||||||
"clips": "Clips",
|
"clips": "Clips",
|
||||||
@@ -267,8 +269,8 @@
|
|||||||
"live_secondary": "Live camera view options",
|
"live_secondary": "Live camera view options",
|
||||||
"event_gallery": "Event gallery",
|
"event_gallery": "Event gallery",
|
||||||
"event_gallery_secondary": "Snapshots & clips gallery options",
|
"event_gallery_secondary": "Snapshots & clips gallery options",
|
||||||
"event_viewer": "Event viewer",
|
"media_viewer": "Media viewer",
|
||||||
"event_viewer_secondary": "Snapshots & clips viewer options",
|
"media_viewer_secondary": "Viewer for static media (clips, snapshots or recordings)",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"image_secondary": "Static image view options",
|
"image_secondary": "Static image view options",
|
||||||
"dimensions": "Dimensions",
|
"dimensions": "Dimensions",
|
||||||
@@ -292,13 +294,21 @@
|
|||||||
"in_progress": "In Progress",
|
"in_progress": "In Progress",
|
||||||
"score": "Score"
|
"score": "Score"
|
||||||
},
|
},
|
||||||
|
"recording": {
|
||||||
|
"events": "Events",
|
||||||
|
"seek": "Seek"
|
||||||
|
},
|
||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
"retain_indefinitely": "Event will be indefinitely retained",
|
"retain_indefinitely": "Event will be indefinitely retained",
|
||||||
"timeline": "See event in timeline"
|
"timeline": "See event in timeline",
|
||||||
|
"no_thumbnail": "No thumbnail available"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"undecodable_response": "Could not decode response from Home Assistant for request",
|
||||||
"empty_response": "Received empty response from Home Assistant for request",
|
"empty_response": "Received empty response from Home Assistant for request",
|
||||||
"invalid_response": "Received invalid response from Home Assistant for request",
|
"invalid_response": "Received invalid response from Home Assistant for request",
|
||||||
|
"failed_response": "Failed to receive response from Home Assistant for request",
|
||||||
|
"failed_sign": "Could not sign Home Assistant URL",
|
||||||
"invalid_keys": "Invalid keys",
|
"invalid_keys": "Invalid keys",
|
||||||
"unknown": "Unknown error",
|
"unknown": "Unknown error",
|
||||||
"troubleshooting": "Check troubleshooting",
|
"troubleshooting": "Check troubleshooting",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"event_gallery": {
|
"event_gallery": {
|
||||||
"min_columns": "Número mínimo de colunas"
|
"min_columns": "Número mínimo de colunas"
|
||||||
},
|
},
|
||||||
"event_viewer": {
|
"media_viewer": {
|
||||||
"auto_play": "Reproduzir mídia automaticamente",
|
"auto_play": "Reproduzir mídia automaticamente",
|
||||||
"auto_unmute": "Ativar mídia automaticamente",
|
"auto_unmute": "Ativar mídia automaticamente",
|
||||||
"draggable": "Visualizador de eventos pode ser arrastado/deslizado",
|
"draggable": "Visualizador de eventos pode ser arrastado/deslizado",
|
||||||
@@ -188,8 +188,8 @@
|
|||||||
"live_secondary": "Opções de visualização da câmera ao vivo",
|
"live_secondary": "Opções de visualização da câmera ao vivo",
|
||||||
"event_gallery": "Galeria de eventos",
|
"event_gallery": "Galeria de eventos",
|
||||||
"event_gallery_secondary": "Opções da galeria de Snapshots e clipes",
|
"event_gallery_secondary": "Opções da galeria de Snapshots e clipes",
|
||||||
"event_viewer": "Visualizador de eventos",
|
"media_viewer": "Visualizador de eventos",
|
||||||
"event_viewer_secondary": "Opções do visualizador de Snapshots e clipes",
|
"media_viewer_secondary": "Opções do visualizador de Snapshots e clipes",
|
||||||
"image": "Imagem",
|
"image": "Imagem",
|
||||||
"image_secondary": "Opções de visualização de imagem estática",
|
"image_secondary": "Opções de visualização de imagem estática",
|
||||||
"dimensions": "Dimensões",
|
"dimensions": "Dimensões",
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
this._player?.unmute();
|
this._player?.unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video (unsupported).
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
this._player?.seek(seconds);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
if (this._video) {
|
||||||
|
this._video.currentTime = seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// Minor modifications from:
|
// Minor modifications from:
|
||||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts
|
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek the video.
|
||||||
|
*/
|
||||||
|
public seek(seconds: number): void {
|
||||||
|
if (this._video) {
|
||||||
|
this._video.currentTime = seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// Minor modifications from:
|
// Minor modifications from:
|
||||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-web-rtc-player.ts
|
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-web-rtc-player.ts
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ img,video {
|
|||||||
|
|
||||||
.embla__slide {
|
.embla__slide {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
:host([direction=vertical]) .embla__slide {
|
:host([direction=vertical]) .embla__slide {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
:host {
|
:host {
|
||||||
--frigate-card-thumbnail-size: 100px;
|
|
||||||
--frigate-card-thumbnail-size-max: 175px;
|
--frigate-card-thumbnail-size-max: 175px;
|
||||||
--frigate-card-thumbnail-details-width: calc(var(--frigate-card-thumbnail-size) + 200px);
|
--frigate-card-thumbnail-details-width: calc(var(--frigate-card-thumbnail-size) + 200px);
|
||||||
}
|
}
|
||||||
+14
-11
@@ -1,8 +1,6 @@
|
|||||||
@use "const.scss";
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
// Hide scrollbar: IE and Edge
|
// Hide scrollbar: IE and Edge
|
||||||
@@ -16,8 +14,8 @@
|
|||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(var(--frigate-card-gallery-columns), minmax(0, 1fr));
|
grid-template-columns: repeat(var(--frigate-card-gallery-columns), minmax(0, 1fr));
|
||||||
grid-auto-rows: var(--frigate-card-thumbnail-size);
|
grid-auto-rows: 1fr;
|
||||||
grid-column-gap: var(--frigate-card-gallery-gap);
|
gap: var(--frigate-card-gallery-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide scrollbar for Chrome, Safari and Opera
|
// Hide scrollbar for Chrome, Safari and Opera
|
||||||
@@ -32,16 +30,21 @@ ha-card {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
color: var(--secondary-text-color, white);
|
color: var(--primary-text-color, white);
|
||||||
border: 2px ridge var(--secondary-text-color, black);
|
border: 2px ridge var(--primary-text-color, black);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: var(--primary-background-color, black);
|
background-color: var(--primary-background-color, black);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
transition: transform 0.2s linear;
|
||||||
|
}
|
||||||
|
ha-card:hover {
|
||||||
|
transform: scale(1.04)
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(:not([details])) ha-card,
|
ha-card, frigate-card-thumbnail {
|
||||||
:host(:not([details])) frigate-card-thumbnail {
|
height: 100%;
|
||||||
aspect-ratio: 1 / 1;
|
max-height: var(--frigate-card-thumbnail-size);
|
||||||
}
|
}
|
||||||
@@ -4,4 +4,5 @@
|
|||||||
|
|
||||||
.embla__slide {
|
.embla__slide {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
// Ensure error messages are selectable.
|
||||||
|
user-select: text;
|
||||||
|
// Safari only has prefixed support.
|
||||||
|
-webkit-user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.wrapper {
|
div.wrapper {
|
||||||
@@ -20,6 +25,7 @@ div.message {
|
|||||||
div.message div.contents {
|
div.message div.contents {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.message div.icon {
|
div.message div.icon {
|
||||||
@@ -37,4 +43,6 @@ div.message div.icon {
|
|||||||
|
|
||||||
.message pre {
|
.message pre {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
box-shadow: 0px 0px 20px 5px black;
|
box-shadow: 0px 0px 20px 5px black;
|
||||||
transition: all 0.2s ease-out;
|
transition: all 0.2s ease-out;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
}
|
}
|
||||||
.controls.thumbnails:hover {
|
.controls.thumbnails:hover {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
@use "const.scss";
|
@use 'const.scss';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
--frigate-card-carousel-thumbnail-opacity: 1.0;
|
--frigate-card-carousel-thumbnail-opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([direction=vertical]) {
|
:host([direction='vertical']) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
:host([direction=horizontal]) {
|
:host([direction='horizontal']) {
|
||||||
// In fullscreen mode, without explicitly setting the height to auto Chrome
|
// In fullscreen mode, without explicitly setting the height to auto Chrome
|
||||||
// will construct a stylesheet with 100% height.
|
// will construct a stylesheet with 100% height.
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -19,14 +19,13 @@
|
|||||||
transition: opacity 0.6s ease;
|
transition: opacity 0.6s ease;
|
||||||
}
|
}
|
||||||
.embla__slide.slide-selected {
|
.embla__slide.slide-selected {
|
||||||
opacity: 1.0;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
frigate-card-thumbnail[details] {
|
frigate-card-thumbnail {
|
||||||
width: var(--frigate-card-thumbnail-details-width);
|
|
||||||
height: var(--frigate-card-thumbnail-size);
|
|
||||||
}
|
|
||||||
frigate-card-thumbnail:not([details]) {
|
|
||||||
width: var(--frigate-card-thumbnail-size);
|
width: var(--frigate-card-thumbnail-size);
|
||||||
height: var(--frigate-card-thumbnail-size);
|
height: var(--frigate-card-thumbnail-size);
|
||||||
}
|
}
|
||||||
|
frigate-card-thumbnail[details] {
|
||||||
|
width: var(--frigate-card-thumbnail-details-width);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,15 @@
|
|||||||
column-gap: 5%;
|
column-gap: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.right, div.left {
|
div.right,
|
||||||
|
div.left {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
div.right {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
div.left {
|
div.left {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -36,6 +40,7 @@ span.heading {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.larger {
|
div.larger,
|
||||||
|
span.larger {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
max-width: var(--frigate-card-thumbnail-size);
|
||||||
|
max-height: var(--frigate-card-thumbnail-size);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// Restrict images to a maximum of thumbnail size.
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img, ha-icon {
|
||||||
|
border-radius: var(--ha-card-border-radius, 4px);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
transition: transform 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-icon {
|
||||||
|
--mdc-icon-size: 50%;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
img:hover {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: var(--frigate-card-thumbnail-size);
|
||||||
|
max-height: var(--frigate-card-thumbnail-size);
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border: 1px solid var(--secondary-color);
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
border-radius: var(--ha-card-border-radius, 4px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
|
||||||
|
transition: transform 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:hover) {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
+12
-27
@@ -1,5 +1,3 @@
|
|||||||
@use "const.scss";
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -7,6 +5,11 @@
|
|||||||
|
|
||||||
// Ensure control icons are relative to the thumbnail.
|
// Ensure control icons are relative to the thumbnail.
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:not([details])) {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([details]) {
|
:host([details]) {
|
||||||
@@ -19,39 +22,21 @@
|
|||||||
background-color: var(--primary-background-color, black);
|
background-color: var(--primary-background-color, black);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
ha-icon {
|
||||||
border-radius: var(--ha-card-border-radius, 4px);
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Restrict images to a maximum of thumbnail size.
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
max-width: var(--frigate-card-thumbnail-size-max);
|
|
||||||
max-height: var(--frigate-card-thumbnail-size-max);
|
|
||||||
|
|
||||||
transition: transform 0.2s linear;
|
|
||||||
}
|
|
||||||
img:hover {
|
|
||||||
transform: scale(1.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon.favorite {
|
ha-icon.favorite {
|
||||||
position: absolute;
|
|
||||||
color: gold;
|
color: gold;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon.timeline {
|
ha-icon.timeline {
|
||||||
position: absolute;
|
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
right: 0px;
|
right: 0px;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frigate-card-thumbnail-details-event, frigate-card-thumbnail-details-recording {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@use 'vis-timeline/dist/vis-timeline-graph2d.css';
|
@use 'vis-timeline/dist/vis-timeline-graph2d.css';
|
||||||
@use 'drawer';
|
@use 'drawer';
|
||||||
|
@use 'const.scss';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -15,6 +16,14 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frigate-card-thumbnail {
|
||||||
|
height: var(--frigate-card-thumbnail-size);
|
||||||
|
width: var(--frigate-card-thumbnail-size);
|
||||||
|
}
|
||||||
|
frigate-card-thumbnail[details] {
|
||||||
|
width: var(--frigate-card-thumbnail-details-width);
|
||||||
|
}
|
||||||
|
|
||||||
div.timeline {
|
div.timeline {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -46,10 +55,19 @@ div.timeline.right-margin {
|
|||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
.vis-item.vis-background {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
.vis-item:hover {
|
.vis-item:not(.vis-background) {
|
||||||
// Float icons upwards when the user hovers over them.
|
cursor: pointer;
|
||||||
z-index: 2;
|
}
|
||||||
|
.vis-item.vis-background, .vis-labelset, .vis-time-axis {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.vis-item:active {
|
||||||
|
cursor: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vis-item.vis-box {
|
.vis-item.vis-box {
|
||||||
@@ -71,6 +89,14 @@ div.timeline.right-margin {
|
|||||||
border-color: var(--secondary-color);
|
border-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give an indication that the user can interact with the axes.
|
||||||
|
.vis-text.vis-minor, .vis-label {
|
||||||
|
transition: background-color 0.5s ease-out;
|
||||||
|
}
|
||||||
|
.vis-text.vis-minor:hover, .vis-label:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
.vis-time-axis .vis-grid.vis-major {
|
.vis-time-axis .vis-grid.vis-major {
|
||||||
border-color: var(--secondary-color);
|
border-color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-13
@@ -48,8 +48,8 @@ export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
|
|||||||
const FRIGATE_CARD_VIEWS = [
|
const FRIGATE_CARD_VIEWS = [
|
||||||
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
|
|
||||||
// Event: A clip or snapshot (timeline may produce mixed media lists).
|
// Media: A generic piece of media (could be clip, snapshot, recording).
|
||||||
'event',
|
'media',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
|
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
|
||||||
@@ -83,7 +83,14 @@ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
|
|||||||
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
||||||
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
||||||
|
|
||||||
export class FrigateCardError extends Error {}
|
export class FrigateCardError extends Error {
|
||||||
|
context?: unknown;
|
||||||
|
|
||||||
|
constructor(message: string, context?: unknown) {
|
||||||
|
super(message);
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action Types (for "Picture Elements" / Menu)
|
* Action Types (for "Picture Elements" / Menu)
|
||||||
@@ -400,7 +407,10 @@ const cameraConfigSchema = z
|
|||||||
|
|
||||||
trigger_by_motion: z.boolean().default(cameraConfigDefault.trigger_by_motion),
|
trigger_by_motion: z.boolean().default(cameraConfigDefault.trigger_by_motion),
|
||||||
trigger_by_occupancy: z.boolean().default(cameraConfigDefault.trigger_by_occupancy),
|
trigger_by_occupancy: z.boolean().default(cameraConfigDefault.trigger_by_occupancy),
|
||||||
trigger_by_entities: z.string().array().default(cameraConfigDefault.trigger_by_entities),
|
trigger_by_entities: z
|
||||||
|
.string()
|
||||||
|
.array()
|
||||||
|
.default(cameraConfigDefault.trigger_by_entities),
|
||||||
})
|
})
|
||||||
.default(cameraConfigDefault);
|
.default(cameraConfigDefault);
|
||||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||||
@@ -508,7 +518,7 @@ const viewConfigDefault = {
|
|||||||
scan: {
|
scan: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
show_trigger_status: true,
|
show_trigger_status: true,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const viewConfigSchema = z
|
const viewConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -525,10 +535,14 @@ const viewConfigSchema = z
|
|||||||
update_entities: z.string().array().optional(),
|
update_entities: z.string().array().optional(),
|
||||||
render_entities: z.string().array().optional(),
|
render_entities: z.string().array().optional(),
|
||||||
dark_mode: z.enum(['on', 'off', 'auto']).optional(),
|
dark_mode: z.enum(['on', 'off', 'auto']).optional(),
|
||||||
scan: z.object({
|
scan: z
|
||||||
enabled: z.boolean().default(viewConfigDefault.scan.enabled),
|
.object({
|
||||||
show_trigger_status: z.boolean().default(viewConfigDefault.scan.show_trigger_status),
|
enabled: z.boolean().default(viewConfigDefault.scan.enabled),
|
||||||
}).default(viewConfigDefault.scan)
|
show_trigger_status: z
|
||||||
|
.boolean()
|
||||||
|
.default(viewConfigDefault.scan.show_trigger_status),
|
||||||
|
})
|
||||||
|
.default(viewConfigDefault.scan),
|
||||||
})
|
})
|
||||||
.merge(actionsSchema)
|
.merge(actionsSchema)
|
||||||
.default(viewConfigDefault);
|
.default(viewConfigDefault);
|
||||||
@@ -970,6 +984,7 @@ const timelineConfigDefault = {
|
|||||||
clustering_threshold: 3,
|
clustering_threshold: 3,
|
||||||
media: 'all' as const,
|
media: 'all' as const,
|
||||||
window_seconds: 60 * 60,
|
window_seconds: 60 * 60,
|
||||||
|
show_recordings: true,
|
||||||
controls: {
|
controls: {
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
mode: 'left' as const,
|
mode: 'left' as const,
|
||||||
@@ -995,6 +1010,7 @@ const timelineConfigSchema = z
|
|||||||
.max(24 * 60 * 60)
|
.max(24 * 60 * 60)
|
||||||
.optional()
|
.optional()
|
||||||
.default(timelineConfigDefault.window_seconds),
|
.default(timelineConfigDefault.window_seconds),
|
||||||
|
show_recordings: z.boolean().optional().default(timelineConfigDefault.show_recordings),
|
||||||
controls: z
|
controls: z
|
||||||
.object({
|
.object({
|
||||||
thumbnails: thumbnailsControlSchema
|
thumbnails: thumbnailsControlSchema
|
||||||
@@ -1059,7 +1075,7 @@ export const frigateCardConfigSchema = z.object({
|
|||||||
view: viewConfigSchema,
|
view: viewConfigSchema,
|
||||||
menu: menuConfigSchema,
|
menu: menuConfigSchema,
|
||||||
live: liveConfigSchema,
|
live: liveConfigSchema,
|
||||||
event_viewer: viewerConfigSchema,
|
media_viewer: viewerConfigSchema,
|
||||||
event_gallery: galleryConfigSchema,
|
event_gallery: galleryConfigSchema,
|
||||||
image: imageConfigSchema,
|
image: imageConfigSchema,
|
||||||
elements: pictureElementsSchema,
|
elements: pictureElementsSchema,
|
||||||
@@ -1085,7 +1101,7 @@ export const frigateCardConfigDefaults = {
|
|||||||
view: viewConfigDefault,
|
view: viewConfigDefault,
|
||||||
menu: menuConfigDefault,
|
menu: menuConfigDefault,
|
||||||
live: liveConfigDefault,
|
live: liveConfigDefault,
|
||||||
event_viewer: viewerConfigDefault,
|
media_viewer: viewerConfigDefault,
|
||||||
event_gallery: galleryConfigDefault,
|
event_gallery: galleryConfigDefault,
|
||||||
image: imageConfigDefault,
|
image: imageConfigDefault,
|
||||||
timeline: timelineConfigDefault,
|
timeline: timelineConfigDefault,
|
||||||
@@ -1130,6 +1146,15 @@ export interface BrowseMediaQueryParameters {
|
|||||||
cameraID?: string;
|
cameraID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BrowseRecordingQueryParameters {
|
||||||
|
clientId: string;
|
||||||
|
cameraName: string;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
hour: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BrowseMediaNeighbors {
|
export interface BrowseMediaNeighbors {
|
||||||
previous: FrigateBrowseMediaSource | null;
|
previous: FrigateBrowseMediaSource | null;
|
||||||
previousIndex: number | null;
|
previousIndex: number | null;
|
||||||
@@ -1165,6 +1190,7 @@ export interface FrigateCardMediaPlayer {
|
|||||||
pause(): void;
|
pause(): void;
|
||||||
mute(): void;
|
mute(): void;
|
||||||
unmute(): void;
|
unmute(): void;
|
||||||
|
seek(seconds: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardHelpers {
|
export interface CardHelpers {
|
||||||
@@ -1180,7 +1206,9 @@ export interface CardHelpers {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const MEDIA_CLASS_PLAYLIST = 'playlist' as const;
|
export const MEDIA_CLASS_PLAYLIST = 'playlist' as const;
|
||||||
|
export const MEDIA_CLASS_VIDEO = 'video' as const;
|
||||||
export const MEDIA_TYPE_PLAYLIST = 'playlist' as const;
|
export const MEDIA_TYPE_PLAYLIST = 'playlist' as const;
|
||||||
|
export const MEDIA_TYPE_VIDEO = 'video' as const;
|
||||||
|
|
||||||
// Recursive type, cannot use type interference:
|
// Recursive type, cannot use type interference:
|
||||||
// See: https://github.com/colinhacks/zod#recursive-types
|
// See: https://github.com/colinhacks/zod#recursive-types
|
||||||
@@ -1212,10 +1240,24 @@ export interface FrigateEvent {
|
|||||||
retain_indefinitely?: boolean;
|
retain_indefinitely?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FrigateRecording {
|
||||||
|
camera: string;
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
|
events: number;
|
||||||
|
|
||||||
|
// Specifies the point at which this recording should be played, the
|
||||||
|
// seek_time is the date of the desired play point, and seek_seconds is the
|
||||||
|
// number of seconds to seek to reach that point.
|
||||||
|
seek_time?: number;
|
||||||
|
seek_seconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FrigateBrowseMediaSource extends BrowseMediaSource {
|
export interface FrigateBrowseMediaSource extends BrowseMediaSource {
|
||||||
children?: FrigateBrowseMediaSource[] | null;
|
children?: FrigateBrowseMediaSource[] | null;
|
||||||
frigate?: {
|
frigate?: {
|
||||||
event: FrigateEvent;
|
event?: FrigateEvent;
|
||||||
|
recording?: FrigateRecording;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1274,7 +1316,7 @@ export type Entity = z.infer<typeof entitySchema>;
|
|||||||
export const extendedEntitySchema = entitySchema.extend({
|
export const extendedEntitySchema = entitySchema.extend({
|
||||||
// Extended entity results.
|
// Extended entity results.
|
||||||
unique_id: z.string().optional(),
|
unique_id: z.string().optional(),
|
||||||
})
|
});
|
||||||
export type ExtendedEntity = z.infer<typeof extendedEntitySchema>;
|
export type ExtendedEntity = z.infer<typeof extendedEntitySchema>;
|
||||||
|
|
||||||
export const entityListSchema = entitySchema.array();
|
export const entityListSchema = entitySchema.array();
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { ExtendedHomeAssistant } from '../types';
|
||||||
|
import { homeAssistantHTTPRequest } from './ha';
|
||||||
|
|
||||||
|
const recordingSummaryHourSchema = z.object({
|
||||||
|
hour: z.preprocess((arg) => Number(arg), z.number().min(0).max(23)),
|
||||||
|
duration: z.number().min(0),
|
||||||
|
events: z.number().min(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordingSummarySchema = z
|
||||||
|
.object({
|
||||||
|
day: z.preprocess((arg) => {
|
||||||
|
// Must provide the hour:minute:second on parsing or Javascript will
|
||||||
|
// assume UTC midnight.
|
||||||
|
return typeof arg === 'string' ? new Date(`${arg} 00:00:00`) : arg;
|
||||||
|
}, z.date()),
|
||||||
|
events: z.number(),
|
||||||
|
hours: recordingSummaryHourSchema.array(),
|
||||||
|
})
|
||||||
|
.array();
|
||||||
|
export type RecordingSummary = z.infer<typeof recordingSummarySchema>;
|
||||||
|
|
||||||
|
const recordingSegmentSchema = z.object({
|
||||||
|
start_time: z.number(),
|
||||||
|
end_time: z.number(),
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
const recordingSegmentsSchema = recordingSegmentSchema.array();
|
||||||
|
export type RecordingSegments = z.infer<typeof recordingSegmentsSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the recordings summary.
|
||||||
|
* @param hass The Home Assistant object.
|
||||||
|
* @param client_id The Frigate client_id.
|
||||||
|
* @param camera_name The Frigate camera name.
|
||||||
|
* @returns A RecordingSummary object.
|
||||||
|
*/
|
||||||
|
export const getRecordingsSummary = async (
|
||||||
|
hass: ExtendedHomeAssistant,
|
||||||
|
client_id: string,
|
||||||
|
camera_name: string,
|
||||||
|
): Promise<RecordingSummary> => {
|
||||||
|
return await homeAssistantHTTPRequest(
|
||||||
|
hass,
|
||||||
|
recordingSummarySchema,
|
||||||
|
`/api/frigate/${client_id}/${camera_name}/recordings/summary`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the recording segments..
|
||||||
|
* @param hass The Home Assistant object.
|
||||||
|
* @param client_id The Frigate client_id.
|
||||||
|
* @param camera_name The Frigate camera name.
|
||||||
|
* @param before The segment low watermark.
|
||||||
|
* @param after The segment high watermark.
|
||||||
|
* @returns A RecordingSegments object.
|
||||||
|
*/
|
||||||
|
export const getRecordingSegments = async (
|
||||||
|
hass: ExtendedHomeAssistant,
|
||||||
|
client_id: string,
|
||||||
|
camera_name: string,
|
||||||
|
before: Date,
|
||||||
|
after: Date,
|
||||||
|
): Promise<RecordingSegments> => {
|
||||||
|
return await homeAssistantHTTPRequest(
|
||||||
|
hass,
|
||||||
|
recordingSegmentsSchema,
|
||||||
|
`/api/frigate/${client_id}/${camera_name}/recordings`,
|
||||||
|
new URLSearchParams({
|
||||||
|
before: String(before.getTime() / 1000),
|
||||||
|
after: String(after.getTime() / 1000),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,15 +8,23 @@ import {
|
|||||||
import { homeAssistantWSRequest } from '.';
|
import { homeAssistantWSRequest } from '.';
|
||||||
import {
|
import {
|
||||||
dispatchErrorMessageEvent,
|
dispatchErrorMessageEvent,
|
||||||
|
dispatchFrigateCardErrorEvent,
|
||||||
dispatchMessageEvent
|
dispatchMessageEvent
|
||||||
} from '../../components/message.js';
|
} from '../../components/message.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import {
|
import {
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
|
BrowseRecordingQueryParameters,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
frigateBrowseMediaSourceSchema, FrigateEvent, MEDIA_CLASS_PLAYLIST,
|
frigateBrowseMediaSourceSchema,
|
||||||
MEDIA_TYPE_PLAYLIST
|
FrigateCardError,
|
||||||
|
FrigateEvent,
|
||||||
|
FrigateRecording,
|
||||||
|
MEDIA_CLASS_PLAYLIST,
|
||||||
|
MEDIA_CLASS_VIDEO,
|
||||||
|
MEDIA_TYPE_PLAYLIST,
|
||||||
|
MEDIA_TYPE_VIDEO
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { View } from '../../view.js';
|
import { View } from '../../view.js';
|
||||||
import { getCameraTitle } from '../camera.js';
|
import { getCameraTitle } from '../camera.js';
|
||||||
@@ -27,7 +35,7 @@ import { getCameraTitle } from '../camera.js';
|
|||||||
* @returns The `event_id` or `null` if not successfully parsed.
|
* @returns The `event_id` or `null` if not successfully parsed.
|
||||||
*/
|
*/
|
||||||
export const getEventID = (media: FrigateBrowseMediaSource): string | null => {
|
export const getEventID = (media: FrigateBrowseMediaSource): string | null => {
|
||||||
return media.frigate?.event.id ?? null;
|
return media.frigate?.event?.id ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +44,7 @@ export const getEventID = (media: FrigateBrowseMediaSource): string | null => {
|
|||||||
* @returns The start time in unix/epoch time, or null if it cannot be determined.
|
* @returns The start time in unix/epoch time, or null if it cannot be determined.
|
||||||
*/
|
*/
|
||||||
export const getEventStartTime = (media: FrigateBrowseMediaSource): number | null => {
|
export const getEventStartTime = (media: FrigateBrowseMediaSource): number | null => {
|
||||||
return media.frigate?.event.start_time ?? null;
|
return media.frigate?.event?.start_time ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -336,7 +344,7 @@ export const fetchLatestMediaAndDispatchViewChange = async (
|
|||||||
try {
|
try {
|
||||||
parent = await multipleBrowseMediaQueryMerged(hass, browseMediaQueryParameters);
|
parent = await multipleBrowseMediaQueryMerged(hass, browseMediaQueryParameters);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(element, (e as Error).message);
|
return dispatchFrigateCardErrorEvent(element, e as FrigateCardError);
|
||||||
}
|
}
|
||||||
const childIndex = getFirstTrueMediaChildIndex(parent);
|
const childIndex = getFirstTrueMediaChildIndex(parent);
|
||||||
if (!parent || !parent.children || childIndex == null) {
|
if (!parent || !parent.children || childIndex == null) {
|
||||||
@@ -376,7 +384,7 @@ export const fetchChildMediaAndDispatchViewChange = async (
|
|||||||
try {
|
try {
|
||||||
parent = await browseMedia(hass, child.media_content_id);
|
parent = await browseMedia(hass, child.media_content_id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(element, (e as Error).message);
|
return dispatchFrigateCardErrorEvent(element, e as FrigateCardError);
|
||||||
}
|
}
|
||||||
|
|
||||||
view
|
view
|
||||||
@@ -409,6 +417,38 @@ export const createEventParentForChildren = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a media video child with a given media_content_id.
|
||||||
|
* @param title The title to use for the child.
|
||||||
|
* @param media_con
|
||||||
|
* @param children The children media items.
|
||||||
|
* @returns A single parent containing the children.
|
||||||
|
*/
|
||||||
|
export const createVideoChild = (
|
||||||
|
title: string,
|
||||||
|
mediaContentID: string,
|
||||||
|
options?: {
|
||||||
|
thumbnail?: string;
|
||||||
|
recording?: FrigateRecording;
|
||||||
|
},
|
||||||
|
): FrigateBrowseMediaSource => {
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
media_class: MEDIA_CLASS_VIDEO,
|
||||||
|
media_content_type: MEDIA_TYPE_VIDEO,
|
||||||
|
media_content_id: mediaContentID,
|
||||||
|
can_play: true,
|
||||||
|
can_expand: false,
|
||||||
|
thumbnail: options?.thumbnail ?? null,
|
||||||
|
children: null,
|
||||||
|
...(options?.recording && {
|
||||||
|
frigate: {
|
||||||
|
recording: options.recording,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience function to convert a timestamp to hours, minutes and seconds
|
* Convenience function to convert a timestamp to hours, minutes and seconds
|
||||||
* string. Heavily inspired by, and returning the same format as, the Frigate
|
* string. Heavily inspired by, and returning the same format as, the Frigate
|
||||||
@@ -436,3 +476,23 @@ export function getEventDurationString(event: FrigateEvent): string {
|
|||||||
duration += `${seconds}s`;
|
duration += `${seconds}s`;
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a recording identifier.
|
||||||
|
* @param hass The HomeAssistant object.
|
||||||
|
* @param params The recording parameters to use in the identifer.
|
||||||
|
* @returns A recording identifier.
|
||||||
|
*/
|
||||||
|
export const generateRecordingIdentifier = (
|
||||||
|
params: BrowseRecordingQueryParameters,
|
||||||
|
): string => {
|
||||||
|
return [
|
||||||
|
'media-source://frigate',
|
||||||
|
params.clientId,
|
||||||
|
'recordings',
|
||||||
|
`${params.year}-${String(params.month).padStart(2, '0')}`,
|
||||||
|
String(params.day).padStart(2, '0'),
|
||||||
|
String(params.hour).padStart(2, '0'),
|
||||||
|
params.cameraName,
|
||||||
|
].join('/');
|
||||||
|
};
|
||||||
|
|||||||
+81
-11
@@ -4,7 +4,9 @@ import { StyleInfo } from 'lit/directives/style-map.js';
|
|||||||
import { ZodSchema } from 'zod';
|
import { ZodSchema } from 'zod';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import {
|
import {
|
||||||
CardHelpers, ExtendedHomeAssistant,
|
CardHelpers,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
|
FrigateCardError,
|
||||||
SignedPath,
|
SignedPath,
|
||||||
signedPathSchema,
|
signedPathSchema,
|
||||||
StateParameters
|
StateParameters
|
||||||
@@ -36,12 +38,16 @@ export async function homeAssistantWSRequest<T>(
|
|||||||
const parseResult = schema.safeParse(response);
|
const parseResult = schema.safeParse(response);
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
const keys = getParseErrorKeys<T>(parseResult.error);
|
const keys = getParseErrorKeys<T>(parseResult.error);
|
||||||
const error_message =
|
const error_message = localize('error.invalid_response');
|
||||||
`${localize('error.invalid_response')}: ${JSON.stringify(request)}. ` +
|
console.warn(
|
||||||
localize('error.invalid_keys') +
|
`${error_message}: ${JSON.stringify(request)}. ${localize(
|
||||||
`: '${keys}'`;
|
'error.invalid_keys',
|
||||||
console.warn(error_message);
|
)}: ${keys}`,
|
||||||
throw new Error(error_message);
|
);
|
||||||
|
throw new FrigateCardError(error_message, {
|
||||||
|
request: request,
|
||||||
|
invalid_keys: keys,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return parseResult.data;
|
return parseResult.data;
|
||||||
}
|
}
|
||||||
@@ -74,6 +80,70 @@ export async function homeAssistantSignPath(
|
|||||||
return hass.hassUrl(response.path);
|
return hass.hassUrl(response.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a HomeAssistant HTTP request. May throw.
|
||||||
|
* @param hass The HomeAssistant object to send the request with.
|
||||||
|
* @param schema The expected Zod schema of the response.
|
||||||
|
* @param request The request to make.
|
||||||
|
* @returns The parsed valid response or null on malformed.
|
||||||
|
*/
|
||||||
|
export async function homeAssistantHTTPRequest<T>(
|
||||||
|
hass: ExtendedHomeAssistant,
|
||||||
|
schema: ZodSchema<T>,
|
||||||
|
url: string,
|
||||||
|
params?: URLSearchParams,
|
||||||
|
): Promise<T> {
|
||||||
|
let signResponse: string | null | undefined;
|
||||||
|
try {
|
||||||
|
signResponse = await homeAssistantSignPath(hass, url);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signResponse) {
|
||||||
|
throw new FrigateCardError(localize('error.failed_sign'), {
|
||||||
|
url: url.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedURL = new URL(signResponse);
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
signedURL.searchParams.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(signedURL.toString());
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new FrigateCardError(localize('error.failed_response'), {
|
||||||
|
url: signedURL.toString(),
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_json;
|
||||||
|
try {
|
||||||
|
raw_json = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
throw new FrigateCardError(localize('error.undecodable_response'), {
|
||||||
|
url: signedURL.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return schema.parse(raw_json);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
throw new FrigateCardError(localize('error.invalid_response'), {
|
||||||
|
url: signedURL.toString(),
|
||||||
|
response: raw_json,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface HassStateDifference {
|
interface HassStateDifference {
|
||||||
entity: string;
|
entity: string;
|
||||||
oldState?: HassEntity;
|
oldState?: HassEntity;
|
||||||
@@ -308,9 +378,9 @@ export const isTriggeredState = (state?: HassEntity): boolean => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get entities from the HASS object.
|
* Get entities from the HASS object.
|
||||||
* @param hass
|
* @param hass
|
||||||
* @param domain
|
* @param domain
|
||||||
* @returns
|
* @returns A list of entities ids.
|
||||||
*/
|
*/
|
||||||
export const getEntitiesFromHASS = (hass: HomeAssistant, domain?: string): string[] => {
|
export const getEntitiesFromHASS = (hass: HomeAssistant, domain?: string): string[] => {
|
||||||
if (!hass) {
|
if (!hass) {
|
||||||
@@ -321,4 +391,4 @@ export const getEntitiesFromHASS = (hass: HomeAssistant, domain?: string): strin
|
|||||||
);
|
);
|
||||||
entities.sort();
|
entities.sort();
|
||||||
return entities;
|
return entities;
|
||||||
}
|
};
|
||||||
|
|||||||
+4
-4
@@ -102,7 +102,7 @@ export class View {
|
|||||||
* Determine if a view is for the media viewer.
|
* Determine if a view is for the media viewer.
|
||||||
*/
|
*/
|
||||||
public isViewerView(): boolean {
|
public isViewerView(): boolean {
|
||||||
return ['clip', 'snapshot', 'event'].includes(this.view);
|
return ['clip', 'snapshot', 'media'].includes(this.view);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,9 +145,9 @@ export class View {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch an event to request a view change.
|
* Dispatch an event to request a view change.
|
||||||
* @param node The element dispatching the event.
|
* @param target The target dispatching the event.
|
||||||
*/
|
*/
|
||||||
public dispatchChangeEvent(node: HTMLElement): void {
|
public dispatchChangeEvent(target: EventTarget): void {
|
||||||
dispatchFrigateCardEvent(node, 'change-view', this);
|
dispatchFrigateCardEvent(target, 'change-view', this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user