Merge pull request #6 from dermotduffy/menu-stay-expanded

Add new menu placement options
This commit is contained in:
Dermot Duffy
2021-08-22 11:32:00 -07:00
committed by GitHub
13 changed files with 315 additions and 227 deletions
-2
View File
@@ -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 * * *"
+1 -1
View File
@@ -4,7 +4,7 @@ name: Manage labels
on: on:
push: push:
branches: branches:
- master - main
workflow_dispatch: workflow_dispatch:
jobs: jobs:
+1 -1
View File
@@ -4,7 +4,7 @@ name: Draft a release note
on: on:
push: push:
branches: branches:
- master - main
workflow_dispatch: workflow_dispatch:
jobs: jobs:
+37 -4
View File
@@ -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
View File
@@ -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'}
+26 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+10
View File
@@ -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(),