@@ -999,7 +1085,6 @@ export class FrigateCard extends LitElement {
// 'frigate-card-elements' to re-render (by being a property).
e.view = this._view;
}}
- @frigate-card:card-action=${this._cardActionHandler.bind(this)}
>
`
diff --git a/src/common.ts b/src/common.ts
index df2fbdf5..a87240aa 100644
--- a/src/common.ts
+++ b/src/common.ts
@@ -3,7 +3,7 @@ import { MessageBase } from 'home-assistant-js-websocket';
import { HomeAssistant } from 'custom-card-helpers';
import { localize } from './localize/localize.js';
import {
- ElementsActionType,
+ ActionType,
ExtendedHomeAssistant,
FrigateCardCustomAction,
frigateCardCustomActionSchema,
@@ -258,29 +258,55 @@ export function isValidMediaShowInfo(info: MediaShowInfo): boolean {
);
}
-export function convertActionToFrigateCardCustomAction(action: ElementsActionType): FrigateCardCustomAction | null {
+/**
+ * Convert a generic Action to a FrigateCardCustomAction if it parses correctly.
+ * @param action The generic action configuration.
+ * @returns A FrigateCardCustomAction or null if it cannot be converted.
+ */
+export function convertActionToFrigateCardCustomAction(
+ action: ActionType,
+): FrigateCardCustomAction | null {
// Parse a custom event as other things could generate ll-custom events that
// are not related to Frigate Card.
const parseResult = frigateCardCustomActionSchema.safeParse(action);
return parseResult.success ? parseResult.data : null;
}
+/**
+ * Create a Frigate card custom action.
+ * @param action The Frigate card action string (e.g. 'fullscreen')
+ * @returns A FrigateCardCustomAction for that action string.
+ */
export function createFrigateCardCustomAction(action: string): FrigateCardCustomAction {
return {
action: 'fire-dom-event',
frigate_card_action: action,
- }
+ };
}
-export function convertLovelaceEventToCardActionEvent(
- node: HTMLElement,
- ev: CustomEvent,
-): void {
- const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
- if (frigateCardAction) {
- ev.stopPropagation();
- dispatchFrigateCardEvent(node, 'card-action', {
- action: frigateCardAction.frigate_card_action,
- });
+/**
+ * Get an action configuration given a config and an interaction (e.g. 'tap').
+ * @param interaction The interaction: `tap`, `hold` or `double_tap`
+ * @param config The configuration containing multiple actions.
+ * @returns The relevant action configuration or null if none found.
+ */
+export function getActionConfigGivenAction(
+ interaction?: string,
+ config?: {
+ hold_action?: ActionType;
+ tap_action?: ActionType;
+ double_tap_action?: ActionType;
+ },
+): ActionType | null {
+ 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;
}
diff --git a/src/components/elements.ts b/src/components/elements.ts
index 366c9055..b92ca112 100644
--- a/src/components/elements.ts
+++ b/src/components/elements.ts
@@ -11,7 +11,6 @@ import {
PictureElements,
} from '../types.js';
import {
- convertLovelaceEventToCardActionEvent,
dispatchErrorMessageEvent,
dispatchFrigateCardEvent,
} from '../common.js';
@@ -184,7 +183,6 @@ export class FrigateCardElements extends LitElement {
.hass=${this._hass}
.view=${this.view}
.elements=${this.elements}
- @ll-custom=${(ev: CustomEvent) => convertLovelaceEventToCardActionEvent(this, ev)}
>
`;
}
diff --git a/src/components/menu.ts b/src/components/menu.ts
index fcf5f0bf..a3468bb4 100644
--- a/src/components/menu.ts
+++ b/src/components/menu.ts
@@ -15,16 +15,13 @@ import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../action-handler-directive.js';
import type {
- CardAction,
- ElementsActionType,
ExtendedHomeAssistant,
MenuButton,
MenuConfig,
} from '../types.js';
import {
convertActionToFrigateCardCustomAction,
- convertLovelaceEventToCardActionEvent,
- dispatchFrigateCardEvent,
+ getActionConfigGivenAction,
shouldUpdateBasedOnHass,
} from '../common.js';
@@ -33,7 +30,9 @@ import menuStyle from '../scss/menu.scss';
export const MENU_HEIGHT = 46;
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
-// A menu for the Frigate card.
+/**
+ * A menu for the FrigateCard.
+ */
@customElement('frigate-card-menu')
export class FrigateCardMenu extends LitElement {
@property({ attribute: false })
@@ -54,54 +53,50 @@ export class FrigateCardMenu extends LitElement {
@property({ attribute: false })
public buttons: MenuButton[] = [];
- protected _interactionHandler(ev: CustomEvent, button: MenuButton): void {
+ /**
+ * Handle an action on a menu button.
+ * @param ev The action event.
+ * @param button The button configuration.
+ */
+ protected _actionHandler(ev: CustomEvent, button: MenuButton): void {
if (!ev) {
return;
}
- const interaction: string = ev.detail.action;
- let action: ElementsActionType | undefined;
+ // These interactions should only be handled by the card, as nothing
+ // upstream has the user-provided configuration.
+ ev.stopPropagation();
- 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) {
+ const interaction: string = ev.detail.action;
+ const action = getActionConfigGivenAction(interaction, button);
+ if (!action || !interaction) {
return;
}
// Determine if this action is a Frigate card action, if so handle it
// internally.
const frigateCardAction = convertActionToFrigateCardCustomAction(action);
- if (frigateCardAction) {
- if (frigateCardAction.frigate_card_action == 'frigate') {
- // If the user presses the frigate button and it's a hide-away menu,
- // then expand the menu and return.
- if (this._menuConfig?.mode.startsWith('hidden-')) {
- this.expand = !this.expand;
- return;
- }
- }
-
- // Collapse menu after the user clicks on something.
- this.expand = false;
-
- dispatchFrigateCardEvent(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);
+ if (
+ frigateCardAction &&
+ frigateCardAction.frigate_card_action == 'frigate' &&
+ this._menuConfig?.mode.startsWith('hidden-')
+ ) {
+ // If the user presses the frigate button and it's a hide-away menu,
+ // then expand the menu and return.
+ this.expand = !this.expand;
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.
+ * @param changedProps The changed properties.
+ * @returns `true` if the menu should be updated, otherwise `false`.
+ */
protected shouldUpdate(changedProps: PropertyValues): boolean {
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
@@ -120,13 +115,21 @@ export class FrigateCardMenu extends LitElement {
return shouldUpdateBasedOnHass(this.hass, oldHass, entities);
}
+ /**
+ * Get the style of emphasized menu items.
+ * @returns A StyleInfo.
+ */
public static getEmphasizedStyle(): StyleInfo {
return {
color: 'var(--primary-color, white)',
};
}
- // Render a menu button.
+ /**
+ * Render a button.
+ * @param button The button configuration to render.
+ * @returns A rendered template or void.
+ */
protected _renderButton(button: MenuButton): TemplateResult | void {
let state: HassEntity | null = null;
let title = button.title;
@@ -173,8 +176,7 @@ export class FrigateCardMenu extends LitElement {
icon=${icon || 'mdi:gesture-tap-button'}
.label=${title || ''}
title=${title || ''}
- @action=${(ev) => this._interactionHandler(ev, button)}
- @ll-custom=${(ev: CustomEvent) => convertLovelaceEventToCardActionEvent(this, ev)}
+ @action=${(ev) => this._actionHandler(ev, button)}
.actionHandler=${actionHandler({
hasHold: hasHold,
hasDoubleClick: hasDoubleClick,
@@ -184,7 +186,10 @@ export class FrigateCardMenu extends LitElement {
`;
}
- // Render the menu.
+ /**
+ * Render the menu.
+ * @returns A rendered template or void.
+ */
protected render(): TemplateResult | void {
if (!this._menuConfig) {
return;
@@ -221,8 +226,10 @@ export class FrigateCardMenu extends LitElement {
`;
}
- // Return compiled CSS styles (thus safe to use with unsafeCSS).
- static get styles(): CSSResultGroup {
+ /**
+ * Get styles.
+ */
+ static get styles(): CSSResultGroup {
return unsafeCSS(menuStyle);
}
}
diff --git a/src/types.ts b/src/types.ts
index 853a6ebc..2fdc2182 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -117,7 +117,7 @@ export const frigateCardCustomActionSchema = customActionSchema.merge(
);
export type FrigateCardCustomAction = z.infer;
-const elementsActionSchema = z.union([
+const actionSchema = z.union([
toggleActionSchema,
callServiceActionSchema,
navigateActionSchema,
@@ -125,16 +125,29 @@ const elementsActionSchema = z.union([
moreInfoActionSchema,
frigateCardCustomActionSchema,
]);
-export type ElementsActionType = z.infer;
+export type ActionType = z.infer;
-const elementsBaseSchema = z.object({
- style: z.object({}).passthrough().optional(),
- title: z.string().nullable().optional(),
- tap_action: elementsActionSchema.optional(),
- hold_action: elementsActionSchema.optional(),
- double_tap_action: elementsActionSchema.optional(),
+const actionBaseSchema = z.object({
+ tap_action: actionSchema.optional(),
+ hold_action: actionSchema.optional(),
+ double_tap_action: actionSchema.optional(),
+}).passthrough();
+export type Actions = z.infer;
+
+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.
*
@@ -322,6 +335,7 @@ const viewConfigSchema = z
.optional()
.default(viewConfigDefault.timeout),
})
+ .merge(actionsSchema)
.default(viewConfigDefault);
/**
@@ -331,6 +345,7 @@ const imageConfigSchema = z
.object({
src: z.string().optional(),
})
+ .merge(actionsSchema)
.optional();
export type ImageViewConfig = z.infer;
@@ -356,6 +371,7 @@ const liveConfigSchema = z
preload: z.boolean().default(liveConfigDefault.preload),
webrtc: webrtcConfigSchema,
})
+ .merge(actionsSchema)
.default(liveConfigDefault);
/**
@@ -430,9 +446,16 @@ const viewerConfigSchema = z
})
.default(viewerConfigDefault.controls),
})
+ .merge(actionsSchema)
.default(viewerConfigDefault);
export type ViewerConfig = z.infer;
+/**
+ * Event gallery configuration section (clips, snapshots).
+ */
+
+const galleryConfigSchema = actionsSchema.optional();
+
/**
* Dimensions configuration section.
*/
@@ -471,6 +494,7 @@ export const frigateCardConfigSchema = z.object({
menu: menuConfigSchema,
live: liveConfigSchema,
event_viewer: viewerConfigSchema,
+ event_gallery: galleryConfigSchema,
image: imageConfigSchema,
elements: pictureElementsSchema,
dimensions: dimensionsConfigSchema,
@@ -537,10 +561,6 @@ export interface Message {
icon?: string;
}
-export interface CardAction {
- action: string;
-}
-
/**
* Home Assistant API types.
*/