From 19291ae1847eccf6fec5b1d95fd9731ed3a61e19 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 10 Nov 2021 07:45:59 -0800 Subject: [PATCH 1/4] Allow custom actions to control the frigate card. --- src/card.ts | 174 ++++++++++++++++++++----------------- src/common.ts | 42 ++++++++- src/components/elements.ts | 7 +- src/components/menu.ts | 133 ++++++++++++++++------------ src/scss/button.scss | 6 +- src/types.ts | 36 ++++---- 6 files changed, 240 insertions(+), 158 deletions(-) diff --git a/src/card.ts b/src/card.ts index 557fa614..f970088f 100644 --- a/src/card.ts +++ b/src/card.ts @@ -15,15 +15,15 @@ import { HomeAssistant, LovelaceCardEditor, getLovelace, - handleAction, } from 'custom-card-helpers'; import screenfull from 'screenfull'; import { z } from 'zod'; import { + CardAction, entitySchema, frigateCardConfigSchema, - MenuInteraction, + GetFrigateCardMenuButtonParameters, RawFrigateCardConfig, } from './types.js'; import type { @@ -38,9 +38,10 @@ import type { import { CARD_VERSION, REPO_URL } from './const.js'; import { FrigateCardElements } from './components/elements.js'; -import { FrigateCardMenu, MENU_HEIGHT } from './components/menu.js'; +import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON, MENU_HEIGHT } from './components/menu.js'; import { View } from './view.js'; import { + createFrigateCardCustomAction, homeAssistantSignPath, homeAssistantWSRequest, isValidMediaShowInfo, @@ -63,7 +64,7 @@ import './patches/ha-hls-player.js'; import cardStyle from './scss/card.scss'; import { ResolvedMediaCache } from './resolved-media.js'; import { BrowseMediaUtil } from './browse-media-util.js'; -import { copyConfig, isConfigUpgradeable, upgradeConfig } from './config-mgmt.js'; +import { isConfigUpgradeable } from './config-mgmt.js'; /** A note on media callbacks: * @@ -191,6 +192,23 @@ export class FrigateCard extends LitElement { } as FrigateCardConfig; } + protected _getFrigateCardMenuButton( + params: GetFrigateCardMenuButtonParameters, + ): MenuButton { + return { + type: 'custom:frigate-card-menu-icon', + title: params.title, + icon: params.icon, + style: params.emphasize ? FrigateCardMenu.getEmphasizedStyle() : {}, + tap_action: params.tap_action + ? createFrigateCardCustomAction(params.tap_action) + : undefined, + hold_action: params.hold_action + ? createFrigateCardCustomAction(params.hold_action) + : undefined, + }; + } + /** * Get the menu buttons to display. * @returns An array of menu buttons. @@ -199,73 +217,86 @@ export class FrigateCard extends LitElement { const buttons: MenuButton[] = []; if (this.config.menu.buttons.frigate) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'frigate', - title: localize('config.menu.buttons.frigate'), - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'frigate', + // Use a magic icon value that the menu will use to render the icon as + // it deems appropriate (certain menu configurations change the menu + // icon for the 'Frigate' button). + icon: FRIGATE_BUTTON_MENU_ICON, + title: localize('config.menu.buttons.frigate'), + }), + ); } if (this.config.menu.buttons.live) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'live', - title: localize('config.view.views.live'), - icon: 'mdi:cctv', - emphasize: this._view.is('live'), - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'live', + title: localize('config.view.views.live'), + icon: 'mdi:cctv', + emphasize: this._view.is('live'), + }), + ); } + if (this.config.menu.buttons.clips) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'clips', - hold_action: 'clip', - title: localize('config.view.views.clips'), - icon: 'mdi:filmstrip', - emphasize: this._view.is('clips'), - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'clips', + hold_action: 'clip', + title: localize('config.view.views.clips'), + icon: 'mdi:filmstrip', + emphasize: this._view.is('clips'), + }), + ); } if (this.config.menu.buttons.snapshots) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'snapshots', - hold_action: 'snapshot', - title: localize('config.view.views.snapshots'), - icon: 'mdi:camera', - emphasize: this._view.is('snapshots'), - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'snapshots', + hold_action: 'snapshot', + title: localize('config.view.views.snapshots'), + icon: 'mdi:camera', + emphasize: this._view.is('snapshots'), + }), + ); } if (this.config.menu.buttons.image) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'image', - title: localize('config.view.views.image'), - icon: 'mdi:image', - emphasize: this._view.is('image'), - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'image', + title: localize('config.view.views.image'), + icon: 'mdi:image', + emphasize: this._view.is('image'), + }), + ); } if (this.config.menu.buttons.download && this._view.isViewerView()) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'download', - title: localize('config.menu.buttons.download'), - icon: 'mdi:download', - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'download', + title: localize('config.menu.buttons.download'), + icon: 'mdi:download', + }), + ); } if (this.config.menu.buttons.frigate_ui && this.config.frigate.url) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'frigate_ui', - title: localize('config.menu.buttons.frigate_ui'), - icon: 'mdi:web', - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'frigate_ui', + title: localize('config.menu.buttons.frigate_ui'), + icon: 'mdi:web', + }), + ); } if (this.config.menu.buttons.fullscreen && screenfull.isEnabled) { - buttons.push({ - type: 'internal-menu-icon', - tap_action: 'fullscreen', - title: localize('config.menu.buttons.fullscreen'), - icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen', - }); + buttons.push( + this._getFrigateCardMenuButton({ + tap_action: 'fullscreen', + title: localize('config.menu.buttons.fullscreen'), + icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen', + }), + ); } return buttons.concat(this._dynamicMenuButtons); } @@ -546,27 +577,11 @@ export class FrigateCard extends LitElement { } /** - * Handle a menu button being clicked. - * @param interaction The interaction that was applied to the button (e.g. - * tap, double_tap, hold). - * @param button The button that was interacted with. + * Handle a request for a card action. + * @param action The action requested (e.g. clips, fullscreen) */ - protected _menuInteractionHandler(event: CustomEvent): void { - const interaction = event.detail.interaction; - const button = event.detail.button; - - if (button.type != 'internal-menu-icon') { - handleAction(this, this._hass as HomeAssistant, button, interaction); - return; - } - - let action: string | undefined = undefined; - if (interaction == 'tap') { - action = button.tap_action; - } else if (interaction == 'hold') { - action = button.hold_action; - } - + protected _cardActionHandler(event: CustomEvent): void { + const action = event.detail.action; if (!action) { return; } @@ -598,7 +613,7 @@ export class FrigateCard extends LitElement { } break; default: - console.warn(`Frigate card received unknown menu action: ${action}`); + console.warn(`Frigate card received unknown card action: ${action}`); } } @@ -648,7 +663,7 @@ export class FrigateCard extends LitElement { .menuConfig=${this.config.menu} .buttons=${this._getMenuButtons()} class="${classMap(classes)}" - @frigate-card:menu-interaction=${this._menuInteractionHandler.bind(this)} + @frigate-card:card-action=${this._cardActionHandler.bind(this)} > `; } @@ -984,6 +999,7 @@ 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 158f962b..df2fbdf5 100644 --- a/src/common.ts +++ b/src/common.ts @@ -3,7 +3,10 @@ import { MessageBase } from 'home-assistant-js-websocket'; import { HomeAssistant } from 'custom-card-helpers'; import { localize } from './localize/localize.js'; import { + ElementsActionType, ExtendedHomeAssistant, + FrigateCardCustomAction, + frigateCardCustomActionSchema, MediaShowInfo, Message, SignedPath, @@ -91,7 +94,11 @@ export async function homeAssistantSignPath( * @param name The name of the Frigate card event to send. * @param detail An optional detail object to attach. */ -export function dispatchFrigateCardEvent(element: HTMLElement, name: string, detail?: T): void { +export function dispatchFrigateCardEvent( + element: HTMLElement, + name: string, + detail?: T, +): void { element.dispatchEvent( new CustomEvent(`frigate-card:${name}`, { bubbles: true, @@ -198,7 +205,7 @@ export function dispatchMessageEvent( * Dispatch an event with an error message to show to the user. * @param element The element to send the event. * @param message The message to show. - */ + */ export function dispatchErrorMessageEvent(element: HTMLElement, message: string): void { dispatchFrigateCardEvent(element, 'message', { message: message, @@ -246,5 +253,34 @@ export function shouldUpdateBasedOnHass( * @returns True if the object is valid, false otherwise. */ export function isValidMediaShowInfo(info: MediaShowInfo): boolean { - return info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF; + return ( + info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF + ); +} + +export function convertActionToFrigateCardCustomAction(action: ElementsActionType): 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; +} + +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, + }); + } } diff --git a/src/components/elements.ts b/src/components/elements.ts index 7dd296d3..366c9055 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -10,7 +10,11 @@ import { MenuStateIcon, PictureElements, } from '../types.js'; -import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; +import { + convertLovelaceEventToCardActionEvent, + dispatchErrorMessageEvent, + dispatchFrigateCardEvent, +} from '../common.js'; import elementsStyle from '../scss/elements.scss'; import { localize } from '../localize/localize.js'; @@ -180,6 +184,7 @@ 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 a18a9f59..fcf5f0bf 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -1,5 +1,5 @@ import { HassEntity } from 'home-assistant-js-websocket'; -import { HomeAssistant, hasAction, stateIcon } from 'custom-card-helpers'; +import { HomeAssistant, handleAction, hasAction, stateIcon } from 'custom-card-helpers'; import { CSSResultGroup, LitElement, @@ -10,21 +10,28 @@ import { } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; -import { styleMap } from 'lit/directives/style-map.js'; +import { StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { actionHandler } from '../action-handler-directive.js'; import type { + CardAction, + ElementsActionType, ExtendedHomeAssistant, MenuButton, MenuConfig, - MenuInteraction, } from '../types.js'; -import { dispatchFrigateCardEvent, shouldUpdateBasedOnHass } from '../common.js'; +import { + convertActionToFrigateCardCustomAction, + convertLovelaceEventToCardActionEvent, + dispatchFrigateCardEvent, + shouldUpdateBasedOnHass, +} from '../common.js'; import menuStyle from '../scss/menu.scss'; export const MENU_HEIGHT = 46; +export const FRIGATE_BUTTON_MENU_ICON = 'frigate'; // A menu for the Frigate card. @customElement('frigate-card-menu') @@ -48,19 +55,50 @@ export class FrigateCardMenu extends LitElement { public buttons: MenuButton[] = []; protected _interactionHandler(ev: CustomEvent, button: MenuButton): void { - if (this._menuConfig?.mode.startsWith('hidden-')) { - if (button.type == 'internal-menu-icon' && button.tap_action === 'frigate') { - this.expand = !this.expand; - return; - } - // Collapse menu after the user clicks on something. - this.expand = false; + if (!ev) { + return; } - dispatchFrigateCardEvent(this, 'menu-interaction', { - interaction: ev.detail.action, - button: button, - }); + const interaction: string = ev.detail.action; + let action: ElementsActionType | undefined; + + 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) { + 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); + return; + } } // Determine whether the menu should be updated. @@ -82,38 +120,47 @@ export class FrigateCardMenu extends LitElement { return shouldUpdateBasedOnHass(this.hass, oldHass, entities); } + public static getEmphasizedStyle(): StyleInfo { + return { + color: 'var(--primary-color, white)', + }; + } + // Render a menu button. protected _renderButton(button: MenuButton): TemplateResult | void { let state: HassEntity | null = null; - let emphasize = false; let title = button.title; let icon = button.icon; - const style = ('style' in button ? button.style : {}) || {}; + let style = ('style' in button ? button.style : {}) || {}; + + if (icon == FRIGATE_BUTTON_MENU_ICON) { + icon = + this._menuConfig?.mode.startsWith('hidden-') && !this.expand + ? 'mdi:alpha-f-box-outline' + : 'mdi:alpha-f-box'; + } if (button.type === 'custom:frigate-card-menu-state-icon') { if (!this.hass) { return; } state = this.hass.states[button.entity]; - emphasize = - !!state && button.state_color && ['on', 'active', 'home'].includes(state.state); + if ( + !!state && + button.state_color && + ['on', 'active', 'home'].includes(state.state) + ) { + style = { ...style, ...FrigateCardMenu.getEmphasizedStyle() }; + } title = title ?? (state?.attributes?.friendly_name || button.entity); icon = icon ?? stateIcon(state); - } else if (button.type === 'internal-menu-icon') { - emphasize = button.emphasize ?? false; } - let hasHold = false; - let hasDoubleClick = false; - - if (button.type != 'internal-menu-icon') { - hasHold = hasAction(button.hold_action); - hasDoubleClick = hasAction(button.double_tap_action); - } + const hasHold = hasAction(button.hold_action); + const hasDoubleClick = hasAction(button.double_tap_action); const classes = { button: true, - emphasize: emphasize, }; // TODO: Upon a safe distance from the release of HA 2021.11 these @@ -127,6 +174,7 @@ export class FrigateCardMenu extends LitElement { .label=${title || ''} title=${title || ''} @action=${(ev) => this._interactionHandler(ev, button)} + @ll-custom=${(ev: CustomEvent) => convertLovelaceEventToCardActionEvent(this, ev)} .actionHandler=${actionHandler({ hasHold: hasHold, hasDoubleClick: hasDoubleClick, @@ -136,16 +184,6 @@ export class FrigateCardMenu extends LitElement { `; } - // Render the Frigate menu button. - protected _renderFrigateButton(button: MenuButton): TemplateResult | void { - const icon = - this._menuConfig?.mode.startsWith('hidden-') && !this.expand - ? 'mdi:alpha-f-box-outline' - : 'mdi:alpha-f-box'; - - return this._renderButton(Object.assign({}, button, { icon: icon })); - } - // Render the menu. protected render(): TemplateResult | void { if (!this._menuConfig) { @@ -153,16 +191,7 @@ export class FrigateCardMenu extends LitElement { } const mode = this._menuConfig.mode; - const isFrigateButton = function (button: MenuButton): boolean { - 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 - // unhide it, just show nothing. - if ( - mode == 'none' || - (mode.startsWith('hidden-') && !this.buttons.find(isFrigateButton)) - ) { + if (mode == 'none') { return; } @@ -187,11 +216,7 @@ export class FrigateCardMenu extends LitElement { return html`
- ${Array.from(this.buttons).map((button) => { - return isFrigateButton(button) - ? this._renderFrigateButton(button) - : this._renderButton(button); - })} + ${Array.from(this.buttons).map((button) => this._renderButton(button))}
`; } diff --git a/src/scss/button.scss b/src/scss/button.scss index ebeaaa88..66cc8ec2 100644 --- a/src/scss/button.scss +++ b/src/scss/button.scss @@ -9,8 +9,4 @@ ha-icon-button.button { --ha-icon-display: block; /* Buttons can always be clicked */ pointer-events: auto; -} - -ha-icon-button.button.emphasize { - color: var(--primary-color, white); -} +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index b3b09b4c..0e37f670 100644 --- a/src/types.ts +++ b/src/types.ts @@ -99,12 +99,21 @@ const moreInfoActionSchema = schemaForType()( action: z.literal('more-info'), }), ); +const customActionSchema = z.object({ + action: z.literal('fire-dom-event'), +}) +export const frigateCardCustomActionSchema = customActionSchema.merge(z.object({ + frigate_card_action: z.string(), +})) +export type FrigateCardCustomAction = z.infer; + const elementsActionSchema = z.union([ toggleActionSchema, callServiceActionSchema, navigateActionSchema, urlActionSchema, moreInfoActionSchema, + frigateCardCustomActionSchema, ]); export type ElementsActionType = z.infer; @@ -244,8 +253,6 @@ const frigateConditionalSchema = z.object({ }); export type FrigateConditional = z.infer; -// 'internalMenuIconSchema' is excluded to disallow the user from manually -// changing the internal menu buttons. const pictureElementSchema = z.union([ menuStateIconSchema, menuIconSchema, @@ -476,20 +483,9 @@ export const frigateCardConfigDefaults = { event_viewer: viewerConfigDefault, }; -// Schema for card (non-user configured) menu icons. -const internalMenuIconSchema = z.object({ - type: z.literal('internal-menu-icon'), - title: z.string(), - icon: z.string().optional(), - emphasize: z.boolean().default(false).optional(), - tap_action: z.string(), - hold_action: z.string().optional(), -}); - const menuButtonSchema = z.union([ menuIconSchema, menuStateIconSchema, - internalMenuIconSchema, ]); export type MenuButton = z.infer; export interface ExtendedHomeAssistant { @@ -506,6 +502,15 @@ export interface BrowseMediaQueryParameters { after?: number; } +export interface GetFrigateCardMenuButtonParameters { + icon: string; + title: string; + tap_action: string; + + hold_action?: string; + emphasize?: boolean; +} + export interface BrowseMediaNeighbors { previous: BrowseMediaSource | null; previousIndex: number | null; @@ -525,9 +530,8 @@ export interface Message { icon?: string; } -export interface MenuInteraction { - interaction: 'hold' | 'tap' | 'double_tap'; - button: MenuButton; +export interface CardAction { + action: string; } /** From f84cbcc50d91f6ddd215434c370d18741f6a7d2e Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 10 Nov 2021 21:09:17 -0800 Subject: [PATCH 2/4] Allow syntactic sugar in config. --- src/card.ts | 18 +++++++++--------- src/types.ts | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/card.ts b/src/card.ts index f970088f..8a887bbc 100644 --- a/src/card.ts +++ b/src/card.ts @@ -377,7 +377,7 @@ export class FrigateCard extends LitElement { * @param error The ZodError object from parsing. * @returns An array of string error paths. */ - protected _getParseErrorPaths(error: z.ZodError): string[] { + protected _getParseErrorPaths(error: z.ZodError): Set | null { /* Zod errors involving unions are complex, as Zod may not be able to tell * where the 'real' error is vs simply a union option not matching. This * function finds all ZodError "issues" that don't have an error with 'type' @@ -388,7 +388,7 @@ export class FrigateCard extends LitElement { * exactly why (or rather Zod simply says it doesn't match any of the * available unions). This usually suggests the user specified an incorrect * type name entirely. */ - let contenders: string[] = []; + const contenders = new Set(); if (error && error.issues) { for (let i = 0; i < error.issues.length; i++) { const issue = error.issues[i]; @@ -396,17 +396,17 @@ export class FrigateCard extends LitElement { const unionErrors = (issue as z.ZodInvalidUnionIssue).unionErrors; for (let j = 0; j < unionErrors.length; j++) { const nestedErrors = this._getParseErrorPaths(unionErrors[j]); - if (nestedErrors.length) { - contenders = contenders.concat(nestedErrors); + if (nestedErrors && nestedErrors.size) { + nestedErrors.forEach(contenders.add, contenders); } } } else if (issue.code == 'invalid_type') { if (issue.path[issue.path.length - 1] == 'type') { - return []; + return null; } - contenders.push(this._getParseErrorPathString(issue.path)); + contenders.add(this._getParseErrorPathString(issue.path)); } else if (issue.code != 'custom') { - contenders.push(this._getParseErrorPathString(issue.path)); + contenders.add(this._getParseErrorPathString(issue.path)); } } } @@ -454,8 +454,8 @@ export class FrigateCard extends LitElement { throw new Error( upgradeMessage + `${localize('error.invalid_configuration')}: ` + - (hint.length - ? JSON.stringify(hint, null, ' ') + (hint && hint.size + ? JSON.stringify([...hint], null, ' ') : localize('error.invalid_configuration_no_hint')), ); } diff --git a/src/types.ts b/src/types.ts index 0e37f670..a7c52b24 100644 --- a/src/types.ts +++ b/src/types.ts @@ -103,6 +103,8 @@ const customActionSchema = z.object({ action: z.literal('fire-dom-event'), }) export const frigateCardCustomActionSchema = customActionSchema.merge(z.object({ + // Syntactic sugar to avoid 'fire-dom-event' as part of an external API. + action: z.literal('fire-dom-event').or(z.literal('custom:frigate-card-action').transform(() => 'fire-dom-event')), frigate_card_action: z.string(), })) export type FrigateCardCustomAction = z.infer; From df79ff80eb9682e7a2a534b08c63c2df57972f86 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 10 Nov 2021 21:31:55 -0800 Subject: [PATCH 3/4] Update README. --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94373822..d57fbfdc 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ This card supports all [Picture Elements](https://www.home-assistant.io/lovelace -### `custom:frigate-card-conditional` +#### `custom:frigate-card-conditional` Parameters for the `custom:frigate-card-conditional` element: @@ -282,6 +282,23 @@ Parameters for the `custom:frigate-card-conditional` element: | `elements` | The elements to render. Can be any supported element, include additional condition or custom elements. | See the [PTZ example below](#frigate-card-conditional-example) for a real-world example. + +### Special Actions + +#### `custom:frigate-card-action` + +| Action name | Description | +| - | - | +| `custom:frigate-card-action` | Call a Frigate Card action. Acceptable values are `frigate`, `clip`, `clips`, `image`, `live`, `snapshot`, `snapshots`, `download`, `frigate_ui`, `fullscreen`.| + +| Value | Description | +| - | - | +| `frigate` | Show/hide the menu or trigger the default view. | +| `clip`, `clips`, `image`, `live`, `snapshot`, `snapshots` | Trigger the named [view](#views).| +|`download`|Download the displayed media.| +|`frigate_ui`|Open the Frigate UI at the configured URL.| +|`fullscreen`|Toggle fullscreen.| + ### Elements Examples #### Menu icons @@ -440,6 +457,29 @@ elements: ``` +#### Triggering card actions + +You can control the card itself with the `custom:frigate-card-action` action. + +
+ Expand: Custom fullscreen button + +This example shows an icon that toggles the card fullscreen mode. + +```yaml +elements: + - type: icon + icon: mdi:fullscreen + style: + left: 40px + top: 40px + tap_action: + action: custom:frigate-card-action + frigate_card_action: fullscreen +``` + +
+ ## Views From 450cb0151f543b3548ac7653a4c2698569d497cb Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 11 Nov 2021 20:08:50 -0800 Subject: [PATCH 4/4] fire-dom-event must be a literal --- src/types.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/types.ts b/src/types.ts index a7c52b24..853a6ebc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ import { CallServiceActionConfig, + CustomActionConfig, LovelaceCard, LovelaceCardEditor, MoreInfoActionConfig, @@ -99,14 +100,21 @@ const moreInfoActionSchema = schemaForType()( action: z.literal('more-info'), }), ); -const customActionSchema = z.object({ - action: z.literal('fire-dom-event'), -}) -export const frigateCardCustomActionSchema = customActionSchema.merge(z.object({ - // Syntactic sugar to avoid 'fire-dom-event' as part of an external API. - action: z.literal('fire-dom-event').or(z.literal('custom:frigate-card-action').transform(() => 'fire-dom-event')), - frigate_card_action: z.string(), -})) +const customActionSchema = schemaForType()( + z.object({ + action: z.literal('fire-dom-event'), + }), +); +export const frigateCardCustomActionSchema = customActionSchema.merge( + z.object({ + // Syntactic sugar to avoid 'fire-dom-event' as part of an external API. + action: z + .literal('custom:frigate-card-action') + .transform((): 'fire-dom-event' => 'fire-dom-event') + .or(z.literal('fire-dom-event')), + frigate_card_action: z.string(), + }), +); export type FrigateCardCustomAction = z.infer; const elementsActionSchema = z.union([ @@ -485,10 +493,7 @@ export const frigateCardConfigDefaults = { event_viewer: viewerConfigDefault, }; -const menuButtonSchema = z.union([ - menuIconSchema, - menuStateIconSchema, -]); +const menuButtonSchema = z.union([menuIconSchema, menuStateIconSchema]); export type MenuButton = z.infer; export interface ExtendedHomeAssistant { hassUrl(path?): string;