Add support for card and view actions.

This commit is contained in:
Dermot Duffy
2021-11-13 20:50:11 -08:00
parent 155e62dcfd
commit fc3033a0fe
6 changed files with 211 additions and 83 deletions
+59 -6
View File
@@ -91,6 +91,7 @@ All variables listed are under a `view:` section.
| - | - | - | | - | - | - |
| `default` | `live` | The view to show in the card by default. See [views](#views) below.| | `default` | `live` | The view to show in the card by default. See [views](#views) below.|
| `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.| | `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.|
| `actions` | | Actions to use for all views, individual actions may be overriden by view-specific actions. See [actions](#actions) below.|
### Menu options ### Menu options
@@ -113,6 +114,7 @@ All variables listed are under a `live:` section.
| `webrtc.url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).| | `webrtc.url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).|
| `webrtc.entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). | | `webrtc.entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). |
| `webrtc.*`| | Any other options in a `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.| | `webrtc.*`| | Any other options in a `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.|
| `actions` | | Actions to use for the `live` view. See [actions](#actions) below.|
#### Available Live Providers #### Available Live Providers
@@ -124,7 +126,7 @@ All variables listed are under a `live:` section.
### Event Viewer options ### Event Viewer options
All variables listed are under a `event_viewer:` section. The `event_viewer` is used for viewing all `clip` and `snapshot` media, in a media carousel. All variables listed are under a `event_viewer:` section.
| Option | Default | Description | | Option | Default | Description |
| - | - | - | | - | - | - |
@@ -133,6 +135,15 @@ All variables listed are under a `event_viewer:` section.
| `draggable` | `true` | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. | | `draggable` | `true` | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
| `controls.next_previous.style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . | | `controls.next_previous.style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
| `controls.next_previous.size` | `48px` | The size of the next/previous controls (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].| | `controls.next_previous.size` | `48px` | The size of the next/previous controls (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].|
| `actions` | | Actions to use for all views that use the `event_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.|
### Event Gallery options
The `event_gallery` is used for providing an overview of all `clips` and `snapshots` in a thumbnail gallery. All variables listed are under a `event_gallery:` section.
| Option | Default | Description |
| - | - | - |
| `actions` | | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.|
### Image options ### Image options
@@ -141,6 +152,7 @@ All variables listed are under a `image:` section.
| Option | Default | Description | | Option | Default | Description |
| - | - | - | | - | - | - |
| `src` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).| | `src` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).|
| `actions` | | Actions to use for the `image` view. See [actions](#actions) below.|
### Dimension options ### Dimension options
@@ -457,6 +469,8 @@ elements:
``` ```
</details> </details>
<a name="frigate-card-action"></a>
#### Triggering card actions #### Triggering card actions
You can control the card itself with the `custom:frigate-card-action` action. You can control the card itself with the `custom:frigate-card-action` action.
@@ -516,10 +530,49 @@ See the [other options](#other-options) above.
Clicking on a snapshot will take the user to a clip that was taken at the ~same Clicking on a snapshot will take the user to a clip that was taken at the ~same
time as the snapshot (if any). time as the snapshot (if any).
### Getting event details <a name="actions"></a>
More details about an event can be found by clicking the 'globe' icon in the ## Card & View Actions
menu, which takes the user to the Frigate page for that event.
Actions may be attached to the card itself, to trigger action when the card
experiences a `tap`, `double_tap` or `hold` event. These actions can be
specified both for the overall card and for individual groups of view.
| Configuration path | Views to which it refers |
| - | - |
| `view.actions` | All (may be overriden by the below) |
| `event_viewer.actions` | `clip`, `snapshot` |
| `event_gallery.actions` | `clips`, `snapshots` |
| `live.actions` | `live` |
| `image.actions` | `image` |
If an action is configured for both the whole card (`view.actions`) and a more
specific view (e.g. `live.actions`) then the actions are merged, with the more
specific overriding the less specific (see example below).
The format for actions is the standard Home Assistant [action
format](https://www.home-assistant.io/lovelace/actions/#tap-action) as well as
the custom [Frigate card action](#frigate-card-action) to trigger card changes.
### Example
In this example, double clicking the card in any view will cause the card to go
into fullscreen mode, **except** when the view is `live` in which case the
office lights are toggled.
```yaml
view:
actions:
double_tap_action:
action: custom:frigate-card-action
frigate_card_action: fullscreen
live:
provider: frigate-jsmpeg
actions:
entity: light.office_main_lights
double_tap_action:
action: toggle
```
## Menu Modes ## Menu Modes
@@ -536,7 +589,7 @@ This card supports several menu configurations.
<a name="yaml-examples"></a> <a name="yaml-examples"></a>
### Example YAML Configuration ## Example YAML Configuration
A configuration that uses WebRTC for live: A configuration that uses WebRTC for live:
@@ -558,7 +611,7 @@ A configuration that shows the latest clip on load, but does not automatically p
view_default: clip view_default: clip
``` ```
### Screenshot: Snapshot / Clip Gallery ## Screenshot: Snapshot / Clip Gallery
Full viewing of clips: Full viewing of clips:
+81 -13
View File
@@ -12,19 +12,23 @@ import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { until } from 'lit/directives/until.js'; import { until } from 'lit/directives/until.js';
import { import {
ActionConfig,
HomeAssistant, HomeAssistant,
LovelaceCardEditor, LovelaceCardEditor,
getLovelace, getLovelace,
handleAction,
hasAction,
} from 'custom-card-helpers'; } from 'custom-card-helpers';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
import { z } from 'zod'; import { z } from 'zod';
import { import {
CardAction, ActionType,
entitySchema,
frigateCardConfigSchema,
GetFrigateCardMenuButtonParameters, GetFrigateCardMenuButtonParameters,
RawFrigateCardConfig, RawFrigateCardConfig,
entitySchema,
frigateCardConfigSchema,
Actions,
} from './types.js'; } from './types.js';
import type { import type {
BrowseMediaQueryParameters, BrowseMediaQueryParameters,
@@ -38,10 +42,16 @@ import type {
import { CARD_VERSION, REPO_URL } from './const.js'; import { CARD_VERSION, REPO_URL } from './const.js';
import { FrigateCardElements } from './components/elements.js'; import { FrigateCardElements } from './components/elements.js';
import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON, MENU_HEIGHT } from './components/menu.js'; import {
FRIGATE_BUTTON_MENU_ICON,
MENU_HEIGHT,
FrigateCardMenu,
} from './components/menu.js';
import { View } from './view.js'; import { View } from './view.js';
import { import {
convertActionToFrigateCardCustomAction,
createFrigateCardCustomAction, createFrigateCardCustomAction,
getActionConfigGivenAction,
homeAssistantSignPath, homeAssistantSignPath,
homeAssistantWSRequest, homeAssistantWSRequest,
isValidMediaShowInfo, isValidMediaShowInfo,
@@ -65,14 +75,15 @@ import cardStyle from './scss/card.scss';
import { ResolvedMediaCache } from './resolved-media.js'; import { ResolvedMediaCache } from './resolved-media.js';
import { BrowseMediaUtil } from './browse-media-util.js'; import { BrowseMediaUtil } from './browse-media-util.js';
import { isConfigUpgradeable } from './config-mgmt.js'; import { isConfigUpgradeable } from './config-mgmt.js';
import { actionHandler } from './action-handler-directive.js';
/** A note on media callbacks: /** A note on media callbacks:
* *
* We need media elements (e.g. <video>, <img> or <canvas>) to callback when: * Media elements (e.g. <video>, <img> or <canvas>) need to callback when:
* - Metadata is loaded / dimensions are known (for aspect-ratio) * - Metadata is loaded / dimensions are known (for aspect-ratio)
* - Media is playing / paused (to avoid reloading) * - Media is playing / paused (to avoid reloading)
* *
* There are a number of different approaches used to attach event handlers to * A number of different approaches used to attach event handlers to
* get these callbacks (which need to be attached directly to the media * get these callbacks (which need to be attached directly to the media
* elements, which may be 'buried' down the DOM): * elements, which may be 'buried' down the DOM):
* - Extend the `ha-hls-player` and `ha-camera-stream` to specify the required * - Extend the `ha-hls-player` and `ha-camera-stream` to specify the required
@@ -83,6 +94,15 @@ import { isConfigUpgradeable } from './config-mgmt.js';
* - Directly specifying hooks (e.g. for snapshot viewing with simple <img> tags) * - Directly specifying hooks (e.g. for snapshot viewing with simple <img> tags)
*/ */
/** A note on action/menu/ll-custom events:
*
* The card supports actions being configured in a number of places (e.g. tap on an
* element, double_tap on a menu item, hold on the live view). These actions are
* handled by handleAction() from custom-card-helpers. For Frigate-card specific
* actions, handleAction() call will result in an ll-custom DOM event being
* fired, which needs to be caught at the card level to handle.
*/
/* eslint no-console: 0 */ /* eslint no-console: 0 */
console.info( console.info(
`%c FRIGATE-HASS-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `, `%c FRIGATE-HASS-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
@@ -106,7 +126,7 @@ console.info(
@customElement('frigate-card') @customElement('frigate-card')
export class FrigateCard extends LitElement { export class FrigateCard extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected _hass: (HomeAssistant & ExtendedHomeAssistant) | null = null; protected _hass?: HomeAssistant & ExtendedHomeAssistant;
@state() @state()
public config!: FrigateCardConfig; public config!: FrigateCardConfig;
@@ -580,11 +600,12 @@ export class FrigateCard extends LitElement {
* Handle a request for a card action. * Handle a request for a card action.
* @param action The action requested (e.g. clips, fullscreen) * @param action The action requested (e.g. clips, fullscreen)
*/ */
protected _cardActionHandler(event: CustomEvent<CardAction>): void { protected _cardActionHandler(event: CustomEvent<ActionType>): void {
const action = event.detail.action; const frigateCardAction = convertActionToFrigateCardCustomAction(event.detail);
if (!action) { if (!frigateCardAction) {
return; return;
} }
const action = frigateCardAction.frigate_card_action;
switch (action) { switch (action) {
case 'frigate': case 'frigate':
@@ -649,6 +670,29 @@ export class FrigateCard extends LitElement {
}, this.config.view.timeout * 1000); }, this.config.view.timeout * 1000);
} }
protected _actionHandler(
ev: CustomEvent,
config?: {
hold_action?: ActionType;
tap_action?: ActionType;
double_tap_action?: ActionType;
},
): void {
const interaction = ev.detail.action;
const node: HTMLElement | null = ev.currentTarget as HTMLElement | null;
if (
config &&
node &&
interaction &&
// Don't call handleAction() unless there is explicitly an action defined
// (as it uses a default that is unhelpful for views that have default
// tap/click actions).
getActionConfigGivenAction(interaction, config)
) {
handleAction(node, this._hass as HomeAssistant, config, ev.detail.action);
}
}
/** /**
* Render the card menu. * Render the card menu.
* @returns A rendered template. * @returns A rendered template.
@@ -663,7 +707,6 @@ export class FrigateCard extends LitElement {
.menuConfig=${this.config.menu} .menuConfig=${this.config.menu}
.buttons=${this._getMenuButtons()} .buttons=${this._getMenuButtons()}
class="${classMap(classes)}" class="${classMap(classes)}"
@frigate-card:card-action=${this._cardActionHandler.bind(this)}
></frigate-card-menu> ></frigate-card-menu>
`; `;
} }
@@ -823,6 +866,21 @@ export class FrigateCard extends LitElement {
} }
} }
protected _getMergedActions(): Actions {
let specificActions: Actions | undefined = undefined;
if (this._view.is('live')) {
specificActions = this.config.live.actions;
} else if (this._view.isGalleryView()) {
specificActions = this.config.event_gallery?.actions;
} else if (this._view.isViewerView()) {
specificActions = this.config.event_viewer.actions;
} else if (this._view.is('image')) {
specificActions = this.config.image?.actions;
}
return { ...this.config.view.actions, ...specificActions };
}
/** /**
* Master render method for the card. * Master render method for the card.
*/ */
@@ -868,7 +926,18 @@ export class FrigateCard extends LitElement {
absolute: padding != null, absolute: padding != null,
}; };
return html` <ha-card @click=${this._interactionHandler}> const actions = this._getMergedActions();
return html` <ha-card
@click=${this._interactionHandler}
.actionHandler=${actionHandler({
hasHold: hasAction(actions.hold_action),
hasDoubleClick: hasAction(actions.double_tap_action),
})}
@action=${(ev: CustomEvent) =>
this._actionHandler(ev, actions)}
@ll-custom=${this._cardActionHandler.bind(this)}
>
${this.config.menu.mode == 'above' ? this._renderMenu() : ''} ${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
<div class="container outer" style="${styleMap(outerStyle)}"> <div class="container outer" style="${styleMap(outerStyle)}">
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}"> <div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
@@ -999,7 +1068,6 @@ export class FrigateCard extends LitElement {
// 'frigate-card-elements' to re-render (by being a property). // 'frigate-card-elements' to re-render (by being a property).
e.view = this._view; e.view = this._view;
}} }}
@frigate-card:card-action=${this._cardActionHandler.bind(this)}
> >
</frigate-card-elements> </frigate-card-elements>
` `
+23 -13
View File
@@ -3,7 +3,7 @@ import { MessageBase } from 'home-assistant-js-websocket';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import { import {
ElementsActionType, ActionType,
ExtendedHomeAssistant, ExtendedHomeAssistant,
FrigateCardCustomAction, FrigateCardCustomAction,
frigateCardCustomActionSchema, frigateCardCustomActionSchema,
@@ -258,7 +258,9 @@ export function isValidMediaShowInfo(info: MediaShowInfo): boolean {
); );
} }
export function convertActionToFrigateCardCustomAction(action: ElementsActionType): FrigateCardCustomAction | null { export function convertActionToFrigateCardCustomAction(
action: ActionType,
): FrigateCardCustomAction | null {
// Parse a custom event as other things could generate ll-custom events that // Parse a custom event as other things could generate ll-custom events that
// are not related to Frigate Card. // are not related to Frigate Card.
const parseResult = frigateCardCustomActionSchema.safeParse(action); const parseResult = frigateCardCustomActionSchema.safeParse(action);
@@ -269,18 +271,26 @@ export function createFrigateCardCustomAction(action: string): FrigateCardCustom
return { return {
action: 'fire-dom-event', action: 'fire-dom-event',
frigate_card_action: action, frigate_card_action: action,
} };
} }
export function convertLovelaceEventToCardActionEvent( export function getActionConfigGivenAction(
node: HTMLElement, interaction?: string,
ev: CustomEvent, config?: {
): void { hold_action?: ActionType;
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail); tap_action?: ActionType;
if (frigateCardAction) { double_tap_action?: ActionType;
ev.stopPropagation(); },
dispatchFrigateCardEvent(node, 'card-action', { ): ActionType | null {
action: frigateCardAction.frigate_card_action, if (!interaction || !config) {
}); return null;
} }
if (interaction == 'tap' && config.tap_action) {
return config.tap_action;
} else if (interaction == 'hold' && config.hold_action) {
return config.hold_action;
} else if (interaction == 'double_tap' && config.double_tap_action) {
return config.double_tap_action;
}
return null;
} }
-2
View File
@@ -11,7 +11,6 @@ import {
PictureElements, PictureElements,
} from '../types.js'; } from '../types.js';
import { import {
convertLovelaceEventToCardActionEvent,
dispatchErrorMessageEvent, dispatchErrorMessageEvent,
dispatchFrigateCardEvent, dispatchFrigateCardEvent,
} from '../common.js'; } from '../common.js';
@@ -184,7 +183,6 @@ export class FrigateCardElements extends LitElement {
.hass=${this._hass} .hass=${this._hass}
.view=${this.view} .view=${this.view}
.elements=${this.elements} .elements=${this.elements}
@ll-custom=${(ev: CustomEvent) => convertLovelaceEventToCardActionEvent(this, ev)}
> >
</frigate-card-elements-core>`; </frigate-card-elements-core>`;
} }
+16 -37
View File
@@ -15,16 +15,13 @@ import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../action-handler-directive.js'; import { actionHandler } from '../action-handler-directive.js';
import type { import type {
CardAction,
ElementsActionType,
ExtendedHomeAssistant, ExtendedHomeAssistant,
MenuButton, MenuButton,
MenuConfig, MenuConfig,
} from '../types.js'; } from '../types.js';
import { import {
convertActionToFrigateCardCustomAction, convertActionToFrigateCardCustomAction,
convertLovelaceEventToCardActionEvent, getActionConfigGivenAction,
dispatchFrigateCardEvent,
shouldUpdateBasedOnHass, shouldUpdateBasedOnHass,
} from '../common.js'; } from '../common.js';
@@ -54,21 +51,13 @@ export class FrigateCardMenu extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public buttons: MenuButton[] = []; public buttons: MenuButton[] = [];
protected _interactionHandler(ev: CustomEvent, button: MenuButton): void { protected _actionHandler(ev: CustomEvent, button: MenuButton): void {
if (!ev) { if (!ev) {
return; return;
} }
const interaction: string = ev.detail.action; const interaction: string = ev.detail.action;
let action: ElementsActionType | undefined; const action = getActionConfigGivenAction(interaction, button);
if (interaction == 'tap') {
action = button.tap_action;
} else if (interaction == 'hold') {
action = button.hold_action;
} else if (interaction == 'double_tap') {
action = button.double_tap_action;
}
if (!action) { if (!action) {
return; return;
} }
@@ -76,29 +65,20 @@ export class FrigateCardMenu extends LitElement {
// Determine if this action is a Frigate card action, if so handle it // Determine if this action is a Frigate card action, if so handle it
// internally. // internally.
const frigateCardAction = convertActionToFrigateCardCustomAction(action); const frigateCardAction = convertActionToFrigateCardCustomAction(action);
if (frigateCardAction) { if (
if (frigateCardAction.frigate_card_action == 'frigate') { frigateCardAction &&
// If the user presses the frigate button and it's a hide-away menu, frigateCardAction.frigate_card_action == 'frigate' &&
// then expand the menu and return. this._menuConfig?.mode.startsWith('hidden-')
if (this._menuConfig?.mode.startsWith('hidden-')) { ) {
this.expand = !this.expand; // If the user presses the frigate button and it's a hide-away menu,
return; // then expand the menu and return.
} this.expand = !this.expand;
}
// Collapse menu after the user clicks on something.
this.expand = false;
dispatchFrigateCardEvent<CardAction>(this, 'card-action', {
action: frigateCardAction.frigate_card_action,
});
}
const node: HTMLElement | null = ev.currentTarget as HTMLElement | null;
if (node) {
handleAction(node, this.hass as HomeAssistant, button, interaction);
return; return;
} }
// Collapse menu after the user clicks on something.
this.expand = false;
handleAction(this, this.hass as HomeAssistant, button, interaction);
} }
// Determine whether the menu should be updated. // Determine whether the menu should be updated.
@@ -173,8 +153,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._interactionHandler(ev, button)} @action=${(ev) => this._actionHandler(ev, button)}
@ll-custom=${(ev: CustomEvent) => convertLovelaceEventToCardActionEvent(this, ev)}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
hasHold: hasHold, hasHold: hasHold,
hasDoubleClick: hasDoubleClick, hasDoubleClick: hasDoubleClick,
+32 -12
View File
@@ -117,7 +117,7 @@ export const frigateCardCustomActionSchema = customActionSchema.merge(
); );
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>; export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
const elementsActionSchema = z.union([ const actionSchema = z.union([
toggleActionSchema, toggleActionSchema,
callServiceActionSchema, callServiceActionSchema,
navigateActionSchema, navigateActionSchema,
@@ -125,16 +125,29 @@ const elementsActionSchema = z.union([
moreInfoActionSchema, moreInfoActionSchema,
frigateCardCustomActionSchema, frigateCardCustomActionSchema,
]); ]);
export type ElementsActionType = z.infer<typeof elementsActionSchema>; export type ActionType = z.infer<typeof actionSchema>;
const elementsBaseSchema = z.object({ const actionBaseSchema = z.object({
style: z.object({}).passthrough().optional(), tap_action: actionSchema.optional(),
title: z.string().nullable().optional(), hold_action: actionSchema.optional(),
tap_action: elementsActionSchema.optional(), double_tap_action: actionSchema.optional(),
hold_action: elementsActionSchema.optional(), }).passthrough();
double_tap_action: elementsActionSchema.optional(), export type Actions = z.infer<typeof actionBaseSchema>;
const actionsSchema = z.object({
// Passthrough to allow (at least) entity/camera_image to go through. This
// card doesn't need these attributes, but handleAction() in
// custom_card_helpers may depending on how the action is configured.
actions: actionBaseSchema.optional(),
}); });
const elementsBaseSchema = actionBaseSchema.merge(
z.object({
style: z.object({}).passthrough().optional(),
title: z.string().nullable().optional(),
}),
);
/** /**
* Picture Element Configuration. * Picture Element Configuration.
* *
@@ -322,6 +335,7 @@ const viewConfigSchema = z
.optional() .optional()
.default(viewConfigDefault.timeout), .default(viewConfigDefault.timeout),
}) })
.merge(actionsSchema)
.default(viewConfigDefault); .default(viewConfigDefault);
/** /**
@@ -331,6 +345,7 @@ const imageConfigSchema = z
.object({ .object({
src: z.string().optional(), src: z.string().optional(),
}) })
.merge(actionsSchema)
.optional(); .optional();
export type ImageViewConfig = z.infer<typeof imageConfigSchema>; export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
@@ -356,6 +371,7 @@ const liveConfigSchema = z
preload: z.boolean().default(liveConfigDefault.preload), preload: z.boolean().default(liveConfigDefault.preload),
webrtc: webrtcConfigSchema, webrtc: webrtcConfigSchema,
}) })
.merge(actionsSchema)
.default(liveConfigDefault); .default(liveConfigDefault);
/** /**
@@ -430,9 +446,16 @@ const viewerConfigSchema = z
}) })
.default(viewerConfigDefault.controls), .default(viewerConfigDefault.controls),
}) })
.merge(actionsSchema)
.default(viewerConfigDefault); .default(viewerConfigDefault);
export type ViewerConfig = z.infer<typeof viewerConfigSchema>; export type ViewerConfig = z.infer<typeof viewerConfigSchema>;
/**
* Event gallery configuration section (clips, snapshots).
*/
const galleryConfigSchema = actionsSchema.optional();
/** /**
* Dimensions configuration section. * Dimensions configuration section.
*/ */
@@ -471,6 +494,7 @@ export const frigateCardConfigSchema = z.object({
menu: menuConfigSchema, menu: menuConfigSchema,
live: liveConfigSchema, live: liveConfigSchema,
event_viewer: viewerConfigSchema, event_viewer: viewerConfigSchema,
event_gallery: galleryConfigSchema,
image: imageConfigSchema, image: imageConfigSchema,
elements: pictureElementsSchema, elements: pictureElementsSchema,
dimensions: dimensionsConfigSchema, dimensions: dimensionsConfigSchema,
@@ -537,10 +561,6 @@ export interface Message {
icon?: string; icon?: string;
} }
export interface CardAction {
action: string;
}
/** /**
* Home Assistant API types. * Home Assistant API types.
*/ */