Access the most recent clip/snapshot by holding down the menu button.
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user