Access the most recent clip/snapshot by holding down the menu button.

This commit is contained in:
Dermot Duffy
2021-10-30 01:34:32 -07:00
parent 409f48137d
commit 1844f94dd1
4 changed files with 70 additions and 42 deletions
+3 -3
View File
@@ -411,15 +411,15 @@ elements:
## Views ## Views
This card supports several different views. This card supports several different views:
| Key | Description | | Key | Description |
| ------------- | --------------------------------------------- | | ------------- | --------------------------------------------- |
|`live` (default)| 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. Can also be accessed by holding down the `snapshots` menu icon.|
|`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. Can also be accessed by holding down the `clips` menu icon.|
|`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view_timeout`).| |`image`|Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view_timeout`).|
### Automatic updates in the `clip` or `snapshot` view ### Automatic updates in the `clip` or `snapshot` view
+45 -22
View File
@@ -20,7 +20,7 @@ import {
import screenfull from 'screenfull'; import screenfull from 'screenfull';
import { z } from 'zod'; import { z } from 'zod';
import { entitySchema, frigateCardConfigSchema } from './types.js'; import { entitySchema, frigateCardConfigSchema, MenuInteraction } from './types.js';
import type { import type {
BrowseMediaQueryParameters, BrowseMediaQueryParameters,
Entity, Entity,
@@ -195,14 +195,14 @@ export class FrigateCard extends LitElement {
if (this.config.menu_buttons?.frigate ?? true) { if (this.config.menu_buttons?.frigate ?? true) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'frigate', tap_action: 'frigate',
title: localize('menu.frigate'), title: localize('menu.frigate'),
}); });
} }
if (this.config.menu_buttons?.live ?? true) { if (this.config.menu_buttons?.live ?? true) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'live', tap_action: 'live',
title: localize('menu.live'), title: localize('menu.live'),
icon: 'mdi:cctv', icon: 'mdi:cctv',
emphasize: this._view.is('live'), emphasize: this._view.is('live'),
@@ -211,7 +211,8 @@ export class FrigateCard extends LitElement {
if (this.config.menu_buttons?.clips ?? true) { if (this.config.menu_buttons?.clips ?? true) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'clips', tap_action: 'clips',
hold_action: 'clip',
title: localize('menu.clips'), title: localize('menu.clips'),
icon: 'mdi:filmstrip', icon: 'mdi:filmstrip',
emphasize: this._view.is('clips'), emphasize: this._view.is('clips'),
@@ -220,7 +221,8 @@ export class FrigateCard extends LitElement {
if (this.config.menu_buttons?.snapshots ?? true) { if (this.config.menu_buttons?.snapshots ?? true) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'snapshots', tap_action: 'snapshots',
hold_action: 'snapshot',
title: localize('menu.snapshots'), title: localize('menu.snapshots'),
icon: 'mdi:camera', icon: 'mdi:camera',
emphasize: this._view.is('snapshots'), emphasize: this._view.is('snapshots'),
@@ -229,7 +231,7 @@ export class FrigateCard extends LitElement {
if (this.config.menu_buttons?.image ?? false) { if (this.config.menu_buttons?.image ?? false) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'image', tap_action: 'image',
title: localize('menu.image'), title: localize('menu.image'),
icon: 'mdi:image', icon: 'mdi:image',
emphasize: this._view.is('image'), emphasize: this._view.is('image'),
@@ -238,7 +240,7 @@ export class FrigateCard extends LitElement {
if (this._view.isViewerView() && (this.config.menu_buttons?.download ?? true)) { if (this._view.isViewerView() && (this.config.menu_buttons?.download ?? true)) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'download', tap_action: 'download',
title: localize('menu.download'), title: localize('menu.download'),
icon: 'mdi:download', icon: 'mdi:download',
}); });
@@ -246,7 +248,7 @@ export class FrigateCard extends LitElement {
if ((this.config.menu_buttons?.frigate_ui ?? true) && this.config.frigate_url) { if ((this.config.menu_buttons?.frigate_ui ?? true) && this.config.frigate_url) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'frigate_ui', tap_action: 'frigate_ui',
title: localize('menu.frigate_ui'), title: localize('menu.frigate_ui'),
icon: 'mdi:web', icon: 'mdi:web',
}); });
@@ -254,7 +256,7 @@ export class FrigateCard extends LitElement {
if ((this.config.menu_buttons?.fullscreen ?? true) && screenfull.isEnabled) { if ((this.config.menu_buttons?.fullscreen ?? true) && screenfull.isEnabled) {
buttons.push({ buttons.push({
type: 'internal-menu-icon', type: 'internal-menu-icon',
card_action: 'fullscreen', tap_action: 'fullscreen',
title: localize('menu.fullscreen'), title: localize('menu.fullscreen'),
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen', icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
}); });
@@ -438,6 +440,10 @@ export class FrigateCard extends LitElement {
} }
} }
/**
* Handle a change view event.
* @param e The change view event.
*/
protected _changeViewHandler(e: CustomEvent<View>): void { protected _changeViewHandler(e: CustomEvent<View>): void {
this._changeView(e.detail); this._changeView(e.detail);
} }
@@ -484,7 +490,7 @@ export class FrigateCard extends LitElement {
this._setMessageAndUpdate({ this._setMessageAndUpdate({
message: localize('error.download_no_media'), message: localize('error.download_no_media'),
type: 'error', type: 'error',
}) });
return; return;
} }
const event_id = BrowseMediaUtil.extractEventID(this._view.media); const event_id = BrowseMediaUtil.extractEventID(this._view.media);
@@ -492,14 +498,14 @@ export class FrigateCard extends LitElement {
this._setMessageAndUpdate({ this._setMessageAndUpdate({
message: localize('error.download_no_event_id'), message: localize('error.download_no_event_id'),
type: 'error', type: 'error',
}) });
return; return;
} }
const path = const path =
`/api/frigate/${this.config.frigate_client_id}` + `/api/frigate/${this.config.frigate_client_id}` +
`/notifications/${event_id}/` + `/notifications/${event_id}/` +
`${this._view.isClipRelatedView() ? 'clip.mp4': 'snapshot.jpg'}` + `${this._view.isClipRelatedView() ? 'clip.mp4' : 'snapshot.jpg'}` +
`?download=true`; `?download=true`;
let response: string | null | undefined; let response: string | null | undefined;
try { try {
@@ -512,7 +518,7 @@ export class FrigateCard extends LitElement {
this._setMessageAndUpdate({ this._setMessageAndUpdate({
message: localize('error.download_sign_failed'), message: localize('error.download_sign_failed'),
type: 'error', type: 'error',
}) });
return; return;
} }
@@ -527,24 +533,41 @@ export class FrigateCard extends LitElement {
/** /**
* Handle a menu button being clicked. * Handle a menu button being clicked.
* @param action The action to be called from the clicked button. * @param interaction The interaction that was applied to the button (e.g.
* @param button The button that was clicked. * tap, double_tap, hold).
* @param button The button that was interacted with.
*/ */
protected _menuActionHandler(action: string, button: MenuButton): void { protected _menuInteractionHandler(event: CustomEvent<MenuInteraction>): void {
const interaction = event.detail.interaction;
const button = event.detail.button;
if (button.type != 'internal-menu-icon') { if (button.type != 'internal-menu-icon') {
handleAction(this, this._hass as HomeAssistant, button, action); handleAction(this, this._hass as HomeAssistant, button, interaction);
return; return;
} }
switch (button.card_action) { let action: string | undefined = undefined;
if (interaction == 'tap') {
action = button.tap_action;
} else if (interaction == 'hold') {
action = button.hold_action;
}
if (!action) {
return;
}
switch (action) {
case 'frigate': case 'frigate':
this._changeView(); this._changeView();
break; break;
case 'clip':
case 'clips':
case 'image': case 'image':
case 'live': case 'live':
case 'clips': case 'snapshot':
case 'snapshots': case 'snapshots':
this._changeView(new View({ view: button.card_action })); this._changeView(new View({ view: action }));
break; break;
case 'download': case 'download':
this._downloadViewerMedia(); this._downloadViewerMedia();
@@ -561,7 +584,7 @@ export class FrigateCard extends LitElement {
} }
break; break;
default: default:
console.warn(`Frigate card received unknown menu action: ${button.card_action}`); console.warn(`Frigate card received unknown menu action: ${action}`);
} }
} }
@@ -609,9 +632,9 @@ export class FrigateCard extends LitElement {
<frigate-card-menu <frigate-card-menu
class="${classMap(classes)}" class="${classMap(classes)}"
.hass=${this._hass} .hass=${this._hass}
.actionCallback=${this._menuActionHandler.bind(this)}
.menuMode=${this.config.menu_mode} .menuMode=${this.config.menu_mode}
.buttons=${this._getMenuButtons()} .buttons=${this._getMenuButtons()}
@frigate-card:menu-interaction=${this._menuInteractionHandler.bind(this)}
></frigate-card-menu> ></frigate-card-menu>
`; `;
} }
+15 -16
View File
@@ -14,14 +14,16 @@ import { styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../action-handler-directive.js'; import { actionHandler } from '../action-handler-directive.js';
import type { ExtendedHomeAssistant, FrigateMenuMode } from '../types.js'; import type {
import { MenuButton } from '../types.js'; ExtendedHomeAssistant,
import { shouldUpdateBasedOnHass } from '../common.js'; FrigateMenuMode,
MenuButton,
MenuInteraction,
} from '../types.js';
import { dispatchFrigateCardEvent, shouldUpdateBasedOnHass } from '../common.js';
import menuStyle from '../scss/menu.scss'; import menuStyle from '../scss/menu.scss';
type FrigateCardMenuCallback = (name: string, button: MenuButton) => void;
export const MENU_HEIGHT = 46; export const MENU_HEIGHT = 46;
// A menu for the Frigate card. // A menu for the Frigate card.
@@ -36,16 +38,12 @@ export class FrigateCardMenu extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected expand = false; protected expand = false;
@property({ attribute: false })
protected actionCallback: FrigateCardMenuCallback | null = null;
@property({ attribute: false }) @property({ attribute: false })
public buttons: MenuButton[] = []; public buttons: MenuButton[] = [];
// Call the callback. protected _interactionHandler(ev: CustomEvent, button: MenuButton): void {
protected _callAction(ev: CustomEvent, button: MenuButton): void {
if (this.menuMode.startsWith('hidden-')) { if (this.menuMode.startsWith('hidden-')) {
if (button.type == 'internal-menu-icon' && button.card_action === 'frigate') { if (button.type == 'internal-menu-icon' && button.tap_action === 'frigate') {
this.expand = !this.expand; this.expand = !this.expand;
return; return;
} }
@@ -53,9 +51,10 @@ export class FrigateCardMenu extends LitElement {
this.expand = false; this.expand = false;
} }
if (this.actionCallback) { dispatchFrigateCardEvent<MenuInteraction>(this, 'menu-interaction', {
this.actionCallback(ev.detail.action, button); interaction: ev.detail.action,
} button: button,
});
} }
// Determine whether the menu should be updated. // Determine whether the menu should be updated.
@@ -118,7 +117,7 @@ export class FrigateCardMenu extends LitElement {
icon=${icon || 'mdi:gesture-tap-button'} icon=${icon || 'mdi:gesture-tap-button'}
.label=${title || ''} .label=${title || ''}
title=${title || ''} title=${title || ''}
@action=${(ev) => this._callAction(ev, button)} @action=${(ev) => this._interactionHandler(ev, button)}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
hasHold: hasHold, hasHold: hasHold,
hasDoubleClick: hasDoubleClick, hasDoubleClick: hasDoubleClick,
@@ -141,7 +140,7 @@ export class FrigateCardMenu extends LitElement {
// Render the menu. // Render the menu.
protected render(): TemplateResult { protected render(): TemplateResult {
const isFrigateButton = function (button: MenuButton): boolean { const isFrigateButton = function (button: MenuButton): boolean {
return button.type === 'internal-menu-icon' && button.card_action === 'frigate'; return button.type === 'internal-menu-icon' && button.tap_action === 'frigate';
}; };
// If the menu is off, or if it's in hidden mode but there's no button to // If the menu is off, or if it's in hidden mode but there's no button to
+7 -1
View File
@@ -358,7 +358,8 @@ const internalMenuIconSchema = z.object({
title: z.string(), title: z.string(),
icon: z.string().optional(), icon: z.string().optional(),
emphasize: z.boolean().default(false).optional(), emphasize: z.boolean().default(false).optional(),
card_action: z.string(), tap_action: z.string(),
hold_action: z.string().optional(),
}); });
const menuButtonSchema = z.union([ const menuButtonSchema = z.union([
@@ -400,6 +401,11 @@ export interface Message {
icon?: string; icon?: string;
} }
export interface MenuInteraction {
interaction: "hold" | "tap" | "double_tap",
button: MenuButton,
}
/** /**
* Home Assistant API types. * Home Assistant API types.
*/ */