Merge pull request #6 from dermotduffy/menu-stay-expanded
Add new menu placement options
This commit is contained in:
@@ -4,9 +4,7 @@ name: 'Build'
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
|
||||||
- main
|
- main
|
||||||
- dev
|
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "17 6 * * *"
|
- cron: "17 6 * * *"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ name: Manage labels
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ name: Draft a release note
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ A full-featured Frigate Lovelace card:
|
|||||||
* Support for filtering events by zone and label.
|
* Support for filtering events by zone and label.
|
||||||
* Motion sensor access.
|
* Motion sensor access.
|
||||||
* Full Lovelace editing support.
|
* Full Lovelace editing support.
|
||||||
|
* Theme friendly.
|
||||||
* **Advanced**: Support for [WebRTC](https://github.com/AlexxIT/WebRTC) live viewing.
|
* **Advanced**: Support for [WebRTC](https://github.com/AlexxIT/WebRTC) live viewing.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -47,7 +48,7 @@ Home Assistant > HACS > Integrations > [...] > Custom Repositories
|
|||||||
Home Assistant > HACS > Integrations > "Explore & Add Integrations" > Frigate Card
|
Home Assistant > HACS > Integrations > "Explore & Add Integrations" > Frigate Card
|
||||||
```
|
```
|
||||||
|
|
||||||
* Add the following to your `configuration.yaml`:
|
* Add the following to `configuration.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
lovelace:
|
lovelace:
|
||||||
@@ -57,7 +58,7 @@ lovelace:
|
|||||||
```
|
```
|
||||||
|
|
||||||
* Restart Home Assistant.
|
* Restart Home Assistant.
|
||||||
* Add your new card to your Lovelace configuration!
|
* Add the new card to the Lovelace configuration!
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ lovelace:
|
|||||||
| `motion_entity` | | A binary sensor to show in the menu (e.g. a Frigate motion binary sensor) and to use to trigger card updates.|
|
| `motion_entity` | | A binary sensor to show in the menu (e.g. a Frigate motion binary sensor) and to use to trigger card updates.|
|
||||||
| `frigate_camera_name` | The string after the "camera." in the `camera_entity` option (above). | This parameter allows the camera name heuristic to be overriden for cases where the entity name does not cleanly map to the Frigate camera name.|
|
| `frigate_camera_name` | The string after the "camera." in the `camera_entity` option (above). | This parameter allows the camera name heuristic to be overriden for cases where the entity name does not cleanly map to the Frigate camera name.|
|
||||||
| `view_default` | `live` | The view to show by default. See [views](#views) below.|
|
| `view_default` | `live` | The view to show by default. See [views](#views) below.|
|
||||||
|
| `menu_mode` | `hidden` | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
||||||
| `view_timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
| `view_timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
||||||
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
| `autoplay_clip` | `false` | Whether or not to autoplay clips in the 'clip' [view](#views). Clips manually chosen in the clips gallery will still autoplay.|
|
||||||
|
|
||||||
@@ -105,15 +107,46 @@ Note: WebRTC must be installed and configured separately (see [details](https://
|
|||||||
|
|
||||||
## Views
|
## Views
|
||||||
|
|
||||||
|
This card supports several different views.
|
||||||
|
|
||||||
| Key | Description |
|
| Key | Description |
|
||||||
| ------------- | --------------------------------------------- |
|
| ------------- | --------------------------------------------- |
|
||||||
|`live`| Shows the live camera view, either the name Frigate view or [WebRTC](#webrtc) if configured.|
|
|`live` (default)| Shows the live camera view, either the name Frigate view or [WebRTC](#webrtc) if configured.|
|
||||||
|`snapshots`|Shows the snapshot gallery for this camera/zone/label.|
|
|`snapshots`|Shows the snapshot gallery for this camera/zone/label.|
|
||||||
|`snapshot`|Shows the most recent snapshot for this camera/zone/label.|
|
|`snapshot`|Shows the most recent snapshot for this camera/zone/label.|
|
||||||
|`clips`|Shows the clip gallery for this camera/zone/label.|
|
|`clips`|Shows the clip gallery for this camera/zone/label.|
|
||||||
|`clip`|Shows the most recent clip for this camera/zone/label.|
|
|`clip`|Shows the most recent clip for this camera/zone/label.|
|
||||||
|
|
||||||
For the `clip` or `snapshot` view, the most recent clip/snapshot is rendered. This will automatically update whenever the state of the `camera_entity` or `motion_entity` changes. In particular, if the desire is to have a live view of the most recent event, you should configure `motion_entity` to a Frigate binary sensor associated with that camera in order to trigger updates more regularly (the underlying camera entity state does not change often, the motion binary sensors do).
|
### Automatic updates in the `clip` or `snapshot` view
|
||||||
|
|
||||||
|
Updates will occur whenever the state of the `camera_entity` or `motion_entity`
|
||||||
|
changes. In particular, if the desire is to have a live view of the most recent
|
||||||
|
event, the user should configure `motion_entity` to a Frigate binary sensor
|
||||||
|
associated with that camera in order to trigger updates more regularly (the
|
||||||
|
underlying camera entity state does not change often, the motion binary sensors
|
||||||
|
do).
|
||||||
|
|
||||||
|
### Getting from a snapshot to a clip
|
||||||
|
|
||||||
|
Clicking on a snapshot will take the user to the clip associated with the
|
||||||
|
snapshot (if any).
|
||||||
|
|
||||||
|
### Getting event details
|
||||||
|
|
||||||
|
More details about an event can be found by clicking the 'globe' icon in the
|
||||||
|
menu, which takes the user to the Frigate page for that event.
|
||||||
|
|
||||||
|
## Menu Modes
|
||||||
|
|
||||||
|
This card supports several menu configurations.
|
||||||
|
|
||||||
|
| Key | Description | Screenshot |
|
||||||
|
| ------------- | --------------------------------------------- | - |
|
||||||
|
|`hidden` (default)| Hide the menu by default, expandable upon clicking the 'F' button. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-hidden.png" alt="Menu hidden" width="400px"> |
|
||||||
|
|`overlay`| Overlay the menu on top of the card contents. The 'F' button shows the default view. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-overlay.png" alt="Menu overlaid" width="400px"> |
|
||||||
|
|`above`| Render the menu above the card. The 'F' button shows the default view. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-above.png" alt="Menu above" width="400px"> |
|
||||||
|
|`below`| Render the menu below the card. The 'F' button shows the default view. | <img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/main/images/menu-mode-below.png" alt="Menu below" width="400px"> |
|
||||||
|
|
||||||
|
|
||||||
<a name="yaml-examples"></a>
|
<a name="yaml-examples"></a>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
+27
-1
@@ -103,6 +103,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
snapshot: 'Latest snapshot',
|
snapshot: 'Latest snapshot',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const menuModes = {
|
||||||
|
'': '',
|
||||||
|
hidden: 'Hidden',
|
||||||
|
overlay: 'Overlay',
|
||||||
|
above: 'Above',
|
||||||
|
below: 'Below',
|
||||||
|
};
|
||||||
|
|
||||||
const liveProvider = {
|
const liveProvider = {
|
||||||
'': '',
|
'': '',
|
||||||
frigate: 'Frigate',
|
frigate: 'Frigate',
|
||||||
@@ -195,6 +203,24 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
})}
|
})}
|
||||||
</paper-listbox>
|
</paper-listbox>
|
||||||
</paper-dropdown-menu>
|
</paper-dropdown-menu>
|
||||||
|
<paper-dropdown-menu
|
||||||
|
label="Menu mode (Optional)"
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
.configValue=${'menu_mode'}
|
||||||
|
>
|
||||||
|
<paper-listbox
|
||||||
|
slot="dropdown-content"
|
||||||
|
.selected=${Object.keys(menuModes).indexOf(
|
||||||
|
this._config?.menu_mode || '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${Object.keys(menuModes).map((key) => {
|
||||||
|
return html`
|
||||||
|
<paper-item .label="${key}"> ${menuModes[key]} </paper-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</paper-listbox>
|
||||||
|
</paper-dropdown-menu>
|
||||||
<paper-input
|
<paper-input
|
||||||
label="View timeout (seconds)"
|
label="View timeout (seconds)"
|
||||||
prevent-invalid-input
|
prevent-invalid-input
|
||||||
@@ -205,7 +231,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
.configValue=${'view_timeout'}
|
.configValue=${'view_timeout'}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></paper-input>
|
></paper-input>
|
||||||
<ha-formfield .label=${`Autoplay clip`}>
|
<ha-formfield .label=${`Autoplay latest clip`}>
|
||||||
<ha-switch
|
<ha-switch
|
||||||
.checked=${this._config?.autoplay_clip === true}
|
.checked=${this._config?.autoplay_clip === true}
|
||||||
.configValue=${'autoplay_clip'}
|
.configValue=${'autoplay_clip'}
|
||||||
|
|||||||
@@ -1,41 +1,45 @@
|
|||||||
.frigate-card-menu-container {
|
/* Base: Not used directly */
|
||||||
position: absolute;
|
.frigate-card-menu-base {
|
||||||
top: 0px;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.frigate-card-menu {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
|
||||||
|
/* Menu div itself does not handle click events. Without this, in overlay
|
||||||
|
mode, the menu div prevents clicking on gallery items 'behind' the overlay.
|
||||||
|
*/
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small 'F' button for hidden menu */
|
||||||
|
.frigate-card-menu-hidden {
|
||||||
|
@extend .frigate-card-menu-base;
|
||||||
|
position: absolute;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.frigate-card-menu-expanded {
|
/* Expanded overlay menu */
|
||||||
@extend .frigate-card-menu;
|
.frigate-card-menu-overlay {
|
||||||
|
@extend .frigate-card-menu-hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background: linear-gradient(90deg, rgba(0,0,0,0.3), rgba(0,0,0,0))
|
||||||
}
|
}
|
||||||
|
|
||||||
.frigate-card-menu-title {
|
/* Full above/below menu */
|
||||||
@extend .frigate-card-menu-expanded;
|
.frigate-card-menu-full {
|
||||||
top: 56px;
|
@extend .frigate-card-menu-base;
|
||||||
height: 1em;
|
width: 100%;
|
||||||
padding-bottom: 0.5em;
|
background: var(--secondary-background-color);
|
||||||
padding-left: 0.5em;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon-button.button {
|
ha-icon-button.button {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
color: white;
|
color: var(--secondary-color, white);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
|
|
||||||
|
/* Buttons can always be clicked */
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-39
@@ -1,23 +1,25 @@
|
|||||||
@use "@material/image-list/mdc-image-list";
|
@use "@material/image-list/mdc-image-list";
|
||||||
@use "@material/image-list";
|
@use "@material/image-list";
|
||||||
|
|
||||||
|
.frigate-card-contents {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.frigate-card-viewer {
|
.frigate-card-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.frigate-card-gallery {
|
.frigate-card-gallery {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: absolute;
|
aspect-ratio: 16 / 9;
|
||||||
|
-ms-overflow-style: none; /* Hide scrollbar: IE and Edge */
|
||||||
|
scrollbar-width: none; /* Hide scrollbar: Firefox */
|
||||||
|
max-height: 277px; /* Safari does not support aspect-ratio yet */
|
||||||
|
}
|
||||||
|
|
||||||
left: 0px;
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
right: 0px;
|
.frigate-card-gallery::-webkit-scrollbar {
|
||||||
top: 0px;
|
display: none;
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
-ms-overflow-style: none; // Hide scrollbar: IE and Edge
|
|
||||||
scrollbar-width: none; // Hide scrollbar: Firefox
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.frigate-card-attention {
|
.frigate-card-attention {
|
||||||
@@ -27,39 +29,13 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
||||||
.frigate-card-gallery::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.frigate-card-image-list {
|
.frigate-card-image-list {
|
||||||
@include image-list.standard-columns(5);
|
@include image-list.standard-columns(5, 0px);
|
||||||
@include image-list.shape-radius(5px);
|
@include image-list.shape-radius(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.frigate-card-image-list-icon-overlay {
|
video, img {
|
||||||
position: absolute;
|
display: block;
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.frigate-card-image-list-highlight {
|
|
||||||
// Put border inside.
|
|
||||||
box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
|
|
||||||
opacity: 0.5;
|
|
||||||
|
|
||||||
border-style: dashed;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.invisible {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.visibile {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-card {
|
ha-card {
|
||||||
@@ -70,4 +46,14 @@ ha-card {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: var(--secondary-background-color, black);
|
||||||
|
|
||||||
|
transform-style: preserve-3d; /* Safari brings video elements forward without this */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Don't drop shadow or have radius for nested cards: webrtc */
|
||||||
|
ha-card ha-card {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
background-color: var(--secondary-background-color, black);
|
||||||
|
}
|
||||||
+188
-157
@@ -26,7 +26,11 @@ import './editor';
|
|||||||
import frigate_card_style from './frigate-hass-card.scss';
|
import frigate_card_style from './frigate-hass-card.scss';
|
||||||
import frigate_card_menu_style from './frigate-hass-card-menu.scss';
|
import frigate_card_menu_style from './frigate-hass-card-menu.scss';
|
||||||
|
|
||||||
import { frigateCardConfigSchema, frigateGetEventsResponseSchema } from './types';
|
import {
|
||||||
|
frigateCardConfigSchema,
|
||||||
|
frigateGetEventsResponseSchema,
|
||||||
|
FrigateMenuMode,
|
||||||
|
} from './types';
|
||||||
import type {
|
import type {
|
||||||
FrigateCardView,
|
FrigateCardView,
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
@@ -95,6 +99,9 @@ function shouldUpdateBasedOnHass(
|
|||||||
export class FrigateCardMenu extends LitElement {
|
export class FrigateCardMenu extends LitElement {
|
||||||
static FRIGATE_CARD_MENU_ID: string = 'frigate-card-menu-id' as const;
|
static FRIGATE_CARD_MENU_ID: string = 'frigate-card-menu-id' as const;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected menuMode: FrigateMenuMode = 'hidden';
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected expand = false;
|
protected expand = false;
|
||||||
|
|
||||||
@@ -107,9 +114,6 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected actionCallback: FrigateCardMenuCallback | null = null;
|
protected actionCallback: FrigateCardMenuCallback | null = null;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
protected heading: string | null = null;
|
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
||||||
if (oldHass) {
|
if (oldHass) {
|
||||||
@@ -122,11 +126,17 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
protected _renderFrigateButton(): TemplateResult {
|
protected _renderFrigateButton(): TemplateResult {
|
||||||
return html` <ha-icon-button
|
return html` <ha-icon-button
|
||||||
class="button"
|
class="button"
|
||||||
icon=${this.expand ? 'mdi:alpha-f-box' : 'mdi:alpha-f-box-outline'}
|
icon=${this.menuMode != 'hidden' || this.expand
|
||||||
|
? 'mdi:alpha-f-box'
|
||||||
|
: 'mdi:alpha-f-box-outline'}
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="Frigate menu"
|
title="Frigate menu"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.expand = !this.expand;
|
if (this.menuMode == 'hidden') {
|
||||||
|
this.expand = !this.expand;
|
||||||
|
} else {
|
||||||
|
this._callAction('default');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
></ha-icon-button>`;
|
></ha-icon-button>`;
|
||||||
}
|
}
|
||||||
@@ -140,10 +150,6 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
|
|
||||||
// Render the menu.
|
// Render the menu.
|
||||||
protected render(): TemplateResult | void | ((part: NodePart) => Promise<void>) {
|
protected render(): TemplateResult | void | ((part: NodePart) => Promise<void>) {
|
||||||
if (!this.expand) {
|
|
||||||
return html` <div class="frigate-card-menu">${this._renderFrigateButton()}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let motionIcon: string | null = null;
|
let motionIcon: string | null = null;
|
||||||
if (this.motionEntity && this.hass) {
|
if (this.motionEntity && this.hass) {
|
||||||
motionIcon =
|
motionIcon =
|
||||||
@@ -152,65 +158,73 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
: 'mdi:walk';
|
: 'mdi:walk';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let menuClass = 'frigate-card-menu-full';
|
||||||
|
if (['hidden', 'overlay'].includes(this.menuMode)) {
|
||||||
|
if (this.menuMode == 'overlay' || this.expand) {
|
||||||
|
menuClass = 'frigate-card-menu-overlay';
|
||||||
|
} else {
|
||||||
|
menuClass = 'frigate-card-menu-hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="frigate-card-menu-container">
|
<div class=${menuClass}>
|
||||||
<div class="frigate-card-menu-expanded">
|
${this._renderFrigateButton()}
|
||||||
${this._renderFrigateButton()}
|
${this.menuMode != 'hidden' || this.expand
|
||||||
<ha-icon-button
|
? html`
|
||||||
class="button"
|
<ha-icon-button
|
||||||
icon="mdi:cctv"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
title="View live"
|
|
||||||
@click=${() => {
|
|
||||||
this.expand = false;
|
|
||||||
this._callAction('live');
|
|
||||||
}}
|
|
||||||
></ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
class="button"
|
|
||||||
icon="mdi:filmstrip"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
title="View clips"
|
|
||||||
@click=${() => {
|
|
||||||
this.expand = false;
|
|
||||||
this._callAction('clips');
|
|
||||||
}}
|
|
||||||
></ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
class="button"
|
|
||||||
icon="mdi:camera"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
title="View snapshots"
|
|
||||||
@click=${() => {
|
|
||||||
this.expand = false;
|
|
||||||
this._callAction('snapshots');
|
|
||||||
}}
|
|
||||||
></ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
class="button"
|
|
||||||
icon="mdi:web"
|
|
||||||
data-toggle="tooltip"
|
|
||||||
title="View Frigate UI"
|
|
||||||
@click=${() => {
|
|
||||||
this.expand = false;
|
|
||||||
this._callAction('frigate-ui');
|
|
||||||
}}
|
|
||||||
></ha-icon-button>
|
|
||||||
${!motionIcon
|
|
||||||
? html``
|
|
||||||
: html` <ha-icon-button
|
|
||||||
data-toggle="tooltip"
|
|
||||||
title="View motion sensor"
|
|
||||||
class="button"
|
class="button"
|
||||||
icon="${motionIcon}"
|
icon="mdi:cctv"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="View live"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.expand = false;
|
this.expand = false;
|
||||||
this._callAction('motion');
|
this._callAction('live');
|
||||||
}}
|
}}
|
||||||
></ha-icon-button>`}
|
></ha-icon-button>
|
||||||
</div>
|
<ha-icon-button
|
||||||
${this.heading
|
class="button"
|
||||||
? html` <div class="frigate-card-menu-title">${this.heading}</div> `
|
icon="mdi:filmstrip"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="View clips"
|
||||||
|
@click=${() => {
|
||||||
|
this.expand = false;
|
||||||
|
this._callAction('clips');
|
||||||
|
}}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
class="button"
|
||||||
|
icon="mdi:camera"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="View snapshots"
|
||||||
|
@click=${() => {
|
||||||
|
this.expand = false;
|
||||||
|
this._callAction('snapshots');
|
||||||
|
}}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
class="button"
|
||||||
|
icon="mdi:web"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="View Frigate UI"
|
||||||
|
@click=${() => {
|
||||||
|
this.expand = false;
|
||||||
|
this._callAction('frigate-ui');
|
||||||
|
}}
|
||||||
|
></ha-icon-button>
|
||||||
|
${!motionIcon
|
||||||
|
? html``
|
||||||
|
: html` <ha-icon-button
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="View motion sensor"
|
||||||
|
class="button"
|
||||||
|
icon="${motionIcon}"
|
||||||
|
@click=${() => {
|
||||||
|
this.expand = false;
|
||||||
|
this._callAction('motion');
|
||||||
|
}}
|
||||||
|
></ha-icon-button>`}
|
||||||
|
`
|
||||||
: ``}
|
: ``}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -260,18 +274,19 @@ export class FrigateCard extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected _viewMode: FrigateCardView = 'live';
|
protected _viewMode: FrigateCardView = 'live';
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
protected _viewEvent: FrigateEvent | null = null;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
protected _showMenu = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
protected _heading: string | null = null;
|
|
||||||
|
|
||||||
protected _interactionTimerID: number | null = null;
|
protected _interactionTimerID: number | null = null;
|
||||||
protected _webrtcElement: any | null = null;
|
protected _webrtcElement: any | null = null;
|
||||||
|
|
||||||
|
// Event specifically requested to be shown by the user.
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected _requestedEvent: FrigateEvent | null = null;
|
||||||
|
|
||||||
|
// Event actually being shown to the user. This may be different from
|
||||||
|
// _requestedEvent when no particular event is requested (e.g. most recent) --
|
||||||
|
// in that case the requestedEvent will be null, but _eventBeingShown will be
|
||||||
|
// the actual event shown.
|
||||||
|
protected _eventBeingShown: FrigateEvent | null = null;
|
||||||
|
|
||||||
// Whether or not there is an active clip being played.
|
// Whether or not there is an active clip being played.
|
||||||
protected _clipPlaying = false;
|
protected _clipPlaying = false;
|
||||||
|
|
||||||
@@ -316,14 +331,24 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this._setViewModeToDefault();
|
this._changeView();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the view mode to the configured default.
|
protected _changeView(
|
||||||
protected _setViewModeToDefault(): void {
|
view?: FrigateCardView | undefined,
|
||||||
this._viewMode = this.config.view_default;
|
event?: FrigateEvent | undefined,
|
||||||
if (['clip', 'snapshot'].includes(this._viewMode)) {
|
): void {
|
||||||
this._viewEvent = null;
|
if (view !== undefined) {
|
||||||
|
this._viewMode = view;
|
||||||
|
} else {
|
||||||
|
this._viewMode = this.config.view_default;
|
||||||
|
if (['clip', 'snapshot'].includes(this.config.view_default)) {
|
||||||
|
this._requestedEvent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._eventBeingShown = null;
|
||||||
|
if (event !== undefined) {
|
||||||
|
this._requestedEvent = event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,9 +485,7 @@ export class FrigateCard extends LitElement {
|
|||||||
class="mdc-image-list__image"
|
class="mdc-image-list__image"
|
||||||
src="data:image/png;base64,${event.thumbnail}"
|
src="data:image/png;base64,${event.thumbnail}"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this._showMenu = false;
|
this._changeView(want_clips ? 'clip' : 'snapshot', event);
|
||||||
this._viewEvent = event;
|
|
||||||
this._viewMode = want_clips ? 'clip' : 'snapshot';
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -534,24 +557,26 @@ export class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
protected _menuActionHandler(name: string): void {
|
protected _menuActionHandler(name: string): void {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
case 'default':
|
||||||
|
this._controlVideos({ stop: true, control_clip: true });
|
||||||
|
this._controlVideos({ stop: true, control_live: true });
|
||||||
|
this._changeView();
|
||||||
|
break;
|
||||||
case 'live':
|
case 'live':
|
||||||
this._controlVideos({ stop: true, control_clip: true });
|
this._controlVideos({ stop: true, control_clip: true });
|
||||||
this._controlVideos({ stop: false, control_live: true });
|
this._controlVideos({ stop: false, control_live: true });
|
||||||
this._viewMode = name;
|
this._changeView(name);
|
||||||
this._heading = null;
|
|
||||||
break;
|
break;
|
||||||
case 'clips':
|
case 'clips':
|
||||||
this._controlVideos({ stop: true, control_live: true });
|
this._controlVideos({ stop: true, control_live: true });
|
||||||
this._viewMode = name;
|
this._changeView(name);
|
||||||
this._heading = null;
|
|
||||||
break;
|
break;
|
||||||
case 'snapshots':
|
case 'snapshots':
|
||||||
this._controlVideos({ stop: true, control_clip: true, control_live: true });
|
this._controlVideos({ stop: true, control_clip: true, control_live: true });
|
||||||
this._viewMode = name;
|
this._changeView(name);
|
||||||
this._heading = null;
|
|
||||||
break;
|
break;
|
||||||
case 'frigate-ui':
|
case 'frigate-ui':
|
||||||
window.open(this.config.frigate_url);
|
window.open(this._getFrigateURLFromContext());
|
||||||
break;
|
break;
|
||||||
case 'motion':
|
case 'motion':
|
||||||
if (this.config.motion_entity) {
|
if (this.config.motion_entity) {
|
||||||
@@ -563,6 +588,13 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _getFrigateURLFromContext(): string {
|
||||||
|
if (this._eventBeingShown) {
|
||||||
|
return `${this.config.frigate_url}/events/${this._eventBeingShown.id}`;
|
||||||
|
}
|
||||||
|
return `${this.config.frigate_url}/cameras/${this.config.frigate_camera_name}`;
|
||||||
|
}
|
||||||
|
|
||||||
protected _getClipURLFromEvent(event: FrigateEvent): string | null {
|
protected _getClipURLFromEvent(event: FrigateEvent): string | null {
|
||||||
if (!event.has_clip) {
|
if (!event.has_clip) {
|
||||||
return null;
|
return null;
|
||||||
@@ -581,8 +613,8 @@ export class FrigateCard extends LitElement {
|
|||||||
protected async _renderClipPlayer(): Promise<TemplateResult> {
|
protected async _renderClipPlayer(): Promise<TemplateResult> {
|
||||||
let event: FrigateEvent, events: FrigateGetEventsResponse;
|
let event: FrigateEvent, events: FrigateGetEventsResponse;
|
||||||
let autoplay = true;
|
let autoplay = true;
|
||||||
if (this._viewEvent) {
|
if (this._requestedEvent) {
|
||||||
event = this._viewEvent;
|
event = this._requestedEvent;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
events = await this._getEvents({
|
events = await this._getEvents({
|
||||||
@@ -612,29 +644,32 @@ export class FrigateCard extends LitElement {
|
|||||||
return this._renderAttentionIcon('mdi:camera-off', 'No recent clip');
|
return this._renderAttentionIcon('mdi:camera-off', 'No recent clip');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._heading = this._getEventTitle(event);
|
this._eventBeingShown = event;
|
||||||
|
|
||||||
return html` <video
|
return html`
|
||||||
class="frigate-card-viewer"
|
<video
|
||||||
muted
|
class="frigate-card-viewer"
|
||||||
controls
|
muted
|
||||||
@play=${() => {
|
controls
|
||||||
this._clipPlaying = true;
|
playsinline
|
||||||
}}
|
@play=${() => {
|
||||||
@pause=${() => {
|
this._clipPlaying = true;
|
||||||
this._clipPlaying = false;
|
}}
|
||||||
}}
|
@pause=${() => {
|
||||||
?autoplay="${autoplay}"
|
this._clipPlaying = false;
|
||||||
>
|
}}
|
||||||
<source src="${clipURL}" type="video/mp4" />
|
?autoplay="${autoplay}"
|
||||||
</video>`;
|
>
|
||||||
|
<source src="${clipURL}" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render a snapshot.
|
// Render a snapshot.
|
||||||
protected async _renderSnapshotViewer(): Promise<TemplateResult> {
|
protected async _renderSnapshotViewer(): Promise<TemplateResult> {
|
||||||
let event: FrigateEvent, events: FrigateGetEventsResponse;
|
let event: FrigateEvent, events: FrigateGetEventsResponse;
|
||||||
if (this._viewEvent) {
|
if (this._requestedEvent) {
|
||||||
event = this._viewEvent;
|
event = this._requestedEvent;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
events = await this._getEvents({
|
events = await this._getEvents({
|
||||||
@@ -657,15 +692,14 @@ export class FrigateCard extends LitElement {
|
|||||||
return this._renderAttentionIcon('mdi:filmstrip-off', 'No recent snapshots');
|
return this._renderAttentionIcon('mdi:filmstrip-off', 'No recent snapshots');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._heading = this._getEventTitle(event);
|
this._eventBeingShown = event;
|
||||||
|
|
||||||
return html` <img
|
return html` <img
|
||||||
class="frigate-card-viewer"
|
class="frigate-card-viewer"
|
||||||
src="${snapshotURL}"
|
src="${snapshotURL}"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
if (event.has_clip) {
|
if (event.has_clip) {
|
||||||
this._viewEvent = event;
|
this._changeView('clip', event);
|
||||||
this._viewMode = 'clip';
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>`;
|
/>`;
|
||||||
@@ -679,16 +713,13 @@ export class FrigateCard extends LitElement {
|
|||||||
return this._renderAttentionIcon('mdi:camera-off', 'No live camera');
|
return this._renderAttentionIcon('mdi:camera-off', 'No live camera');
|
||||||
}
|
}
|
||||||
if (this._webrtcElement) {
|
if (this._webrtcElement) {
|
||||||
return html` <div class=${this._viewMode == 'live' ? 'visible' : 'invisible'}>
|
return html`${this._webrtcElement}`;
|
||||||
${this._webrtcElement}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
return html` <ha-camera-stream
|
return html` <ha-camera-stream
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.stateObj=${this._hass.states[this.config.camera_entity]}
|
.stateObj=${this._hass.states[this.config.camera_entity]}
|
||||||
.controls=${true}
|
.controls=${true}
|
||||||
.muted=${true}
|
.muted=${true}
|
||||||
class=${this._viewMode == 'live' ? 'visible' : 'invisible'}
|
|
||||||
>
|
>
|
||||||
</ha-camera-stream>`;
|
</ha-camera-stream>`;
|
||||||
}
|
}
|
||||||
@@ -703,10 +734,22 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
this._interactionTimerID = window.setTimeout(() => {
|
this._interactionTimerID = window.setTimeout(() => {
|
||||||
this._interactionTimerID = null;
|
this._interactionTimerID = null;
|
||||||
this._setViewModeToDefault();
|
this._changeView();
|
||||||
}, this.config.view_timeout * 1000);
|
}, this.config.view_timeout * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _renderMenu(): TemplateResult | void {
|
||||||
|
return html`
|
||||||
|
<frigate-card-menu
|
||||||
|
id="${FrigateCardMenu.FRIGATE_CARD_MENU_ID}"
|
||||||
|
.motionEntity=${this.config.motion_entity}
|
||||||
|
.hass=${this._hass}
|
||||||
|
.actionCallback=${this._menuActionHandler.bind(this)}
|
||||||
|
.menuMode=${this.config.menu_mode}
|
||||||
|
></frigate-card-menu>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Render the call (master render method).
|
// Render the call (master render method).
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (this.config.show_warning) {
|
if (this.config.show_warning) {
|
||||||
@@ -715,47 +758,35 @@ export class FrigateCard extends LitElement {
|
|||||||
if (this.config.show_error) {
|
if (this.config.show_error) {
|
||||||
return this._showError(localize('common.show_error'));
|
return this._showError(localize('common.show_error'));
|
||||||
}
|
}
|
||||||
// Note: Menu is rendered last to allow heading to be set by render methods.
|
return html` <ha-card @click=${this._interactionHandler}>
|
||||||
return html`
|
${this.config.menu_mode != 'below' ? this._renderMenu() : ''}
|
||||||
<ha-card @click=${this._interactionHandler}>
|
<div class="frigate-card-contents">
|
||||||
</frigate-card-menu>
|
${this._viewMode == 'clips'
|
||||||
${
|
? html`<div class="frigate-card-gallery">
|
||||||
this._viewMode == 'clips'
|
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||||
? html`<div class="frigate-card-gallery">
|
</div>`
|
||||||
${until(this._renderEvents(), this._renderProgressIndicator())}
|
: ``}
|
||||||
</div>`
|
${this._viewMode == 'snapshots'
|
||||||
: ``
|
? html`<div class="frigate-card-gallery">
|
||||||
}
|
${until(this._renderEvents(), this._renderProgressIndicator())}
|
||||||
${
|
</div>`
|
||||||
this._viewMode == 'snapshots'
|
: ``}
|
||||||
? html`<div class="frigate-card-gallery">
|
${this._viewMode == 'clip'
|
||||||
${until(this._renderEvents(), this._renderProgressIndicator())}
|
? html`<div class="frigate-card-viewer">
|
||||||
</div>`
|
${until(this._renderClipPlayer(), this._renderProgressIndicator())}
|
||||||
: ``
|
</div>`
|
||||||
}
|
: ``}
|
||||||
${
|
${this._viewMode == 'snapshot'
|
||||||
this._viewMode == 'clip'
|
? html`<div class="frigate-card-viewer">
|
||||||
? html`<div class="frigate-card-viewer">
|
${until(this._renderSnapshotViewer(), this._renderProgressIndicator())}
|
||||||
${until(this._renderClipPlayer(), this._renderProgressIndicator())}
|
</div>`
|
||||||
</div>`
|
: ``}
|
||||||
: ``
|
${this._viewMode == 'live'
|
||||||
}
|
? html`<div class="frigate-card-viewer">${this._renderLiveViewer()}</div>`
|
||||||
${
|
: ``}
|
||||||
this._viewMode == 'snapshot'
|
</div>
|
||||||
? html`<div class="frigate-card-viewer">
|
${this.config.menu_mode == 'below' ? this._renderMenu() : ''}
|
||||||
${until(this._renderSnapshotViewer(), this._renderProgressIndicator())}
|
</ha-card>`;
|
||||||
</div>`
|
|
||||||
: ``
|
|
||||||
}
|
|
||||||
${this._renderLiveViewer()}
|
|
||||||
<frigate-card-menu
|
|
||||||
id="${FrigateCardMenu.FRIGATE_CARD_MENU_ID}"
|
|
||||||
.motionEntity=${this.config.motion_entity}
|
|
||||||
.hass=${this._hass}
|
|
||||||
.actionCallback=${this._menuActionHandler.bind(this)}
|
|
||||||
.heading=${this._heading}
|
|
||||||
></frigate-card-menu>
|
|
||||||
</ha-card>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a warning card.
|
// Show a warning card.
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ export const FRIGATE_CARD_VIEWS = [
|
|||||||
] as const;
|
] as const;
|
||||||
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
|
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
|
||||||
|
|
||||||
|
export const FRIGATE_MENU_MODES = [
|
||||||
|
'hidden',
|
||||||
|
'overlay',
|
||||||
|
'above',
|
||||||
|
'below',
|
||||||
|
] as const;
|
||||||
|
export type FrigateMenuMode = typeof FRIGATE_MENU_MODES[number];
|
||||||
|
|
||||||
|
|
||||||
export const frigateCardConfigSchema = z.object({
|
export const frigateCardConfigSchema = z.object({
|
||||||
camera_entity: z.string(),
|
camera_entity: z.string(),
|
||||||
motion_entity: z.string().optional(),
|
motion_entity: z.string().optional(),
|
||||||
@@ -45,6 +54,7 @@ export const frigateCardConfigSchema = z.object({
|
|||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
zone: z.string().optional(),
|
zone: z.string().optional(),
|
||||||
autoplay_clip: z.boolean().default(false),
|
autoplay_clip: z.boolean().default(false),
|
||||||
|
menu_mode: z.enum(FRIGATE_MENU_MODES).optional().default('hidden'),
|
||||||
|
|
||||||
// Stock lovelace card config.
|
// Stock lovelace card config.
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user