diff --git a/README.md b/README.md
index c9261299..a81fd506 100644
--- a/README.md
+++ b/README.md
@@ -497,6 +497,10 @@ See the [action
documentation](https://www.home-assistant.io/lovelace/actions/#hold-action) for
more information on the action options available.
+**Note**: The Frigate Card allows either a single action (as in stock Home
+Assistant) or list of actions to be defined for each class of user interaction
+(e.g. `tap`, `double_tap`, `hold`, etc). See [an example of multiple actions](#example-multiple-actions).
+
### Special Elements
This card supports all [Picture Elements](https://www.home-assistant.io/lovelace/picture-elements/#icon-element) using the same syntax. The card also supports a handful of custom special elements to add special Frigate card functionality.
@@ -551,16 +555,17 @@ Parameters for the `custom:frigate-card-conditional` element:
| 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`.|
+| `custom:frigate-card-action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `snapshot`, `snapshots`, `download`, `frigate_ui`, `fullscreen`, `camera_select`, `menu_toggle`.|
| Value | Description |
| - | - |
-| `frigate` | Show/hide the menu or trigger the default view. |
+| `default` | 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.|
|`camera_select`|Select a given camera. Takes a single additional `camera` parameter with the [camera ID](#camera-ids) of the camera to select.|
+|`menu_toggle` | Show/hide the menu (for `hidden-*` mode menus). |
@@ -1111,6 +1116,29 @@ image:
```
+
+
+### Defining multiple actions for Elements
+
+
+ Expand: Changing camera and view simultaneously
+
+This example shows how to configure multiple actions for a single Frigate card user interaction, in this case both selecting a different camera and changing the view on `tap`.
+
+```yaml
+[...]
+elements:
+ - type: custom:frigate-card-menu-icon
+ icon: mdi:chair-rolling
+ tap_action:
+ - action: custom:frigate-card-action
+ frigate_card_action: camera_select
+ camera: camera.office
+ - action: custom:frigate-card-action
+ frigate_card_action: live
+```
+
+
## Card Refreshes
diff --git a/src/card.ts b/src/card.ts
index 8c3eacf4..d6743a36 100644
--- a/src/card.ts
+++ b/src/card.ts
@@ -14,7 +14,6 @@ import {
HomeAssistant,
LovelaceCardEditor,
getLovelace,
- hasAction,
} from 'custom-card-helpers';
import screenfull from 'screenfull';
import { z } from 'zod';
@@ -23,10 +22,10 @@ import {
Actions,
ActionType,
CameraConfig,
- GetFrigateCardMenuButtonParameters,
RawFrigateCardConfig,
entitySchema,
frigateCardConfigSchema,
+ FrigateCardCustomAction,
} from './types.js';
import type {
Entity,
@@ -46,6 +45,7 @@ import {
convertActionToFrigateCardCustomAction,
createFrigateCardCustomAction,
frigateCardHandleAction,
+ frigateCardHasAction,
getActionConfigGivenAction,
getCameraIcon,
getCameraTitle,
@@ -256,28 +256,6 @@ export class FrigateCard extends LitElement {
};
}
- /**
- * Get a FrigateCard MenuButton given a set of parameters.
- * @param params Menu button parameters.
- * @returns A MenuButton.
- */
- protected _getFrigateCardMenuButton(
- params: GetFrigateCardMenuButtonParameters,
- ): MenuButton {
- return {
- type: 'custom:frigate-card-menu-icon',
- title: params.title,
- icon: params.icon,
- style: params.emphasize ? this._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.
@@ -286,16 +264,17 @@ export class FrigateCard extends LitElement {
const buttons: MenuButton[] = [];
if (this._getConfig().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'),
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.menu.buttons.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,
+ tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu)
+ ? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
+ : (createFrigateCardCustomAction('default') as FrigateCardCustomAction),
+ });
}
if (
@@ -323,56 +302,58 @@ export class FrigateCard extends LitElement {
}
if (this._getConfig().menu.buttons.live) {
- buttons.push(
- this._getFrigateCardMenuButton({
- tap_action: 'live',
- title: localize('config.view.views.live'),
- icon: 'mdi:cctv',
- emphasize: this._view?.is('live'),
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.view.views.live'),
+ icon: 'mdi:cctv',
+ style: this._view?.is('live') ? this._getEmphasizedStyle() : {},
+ tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
+ });
}
if (this._getConfig().menu.buttons.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'),
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.view.views.clips'),
+ icon: 'mdi:filmstrip',
+ style: this._view?.is('clips') ? this._getEmphasizedStyle() : {},
+ tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
+ hold_action: createFrigateCardCustomAction('clip') as FrigateCardCustomAction,
+ });
}
+
if (this._getConfig().menu.buttons.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'),
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.view.views.snapshots'),
+ icon: 'mdi:camera',
+ style: this._view?.is('snapshots') ? this._getEmphasizedStyle() : {},
+ tap_action: createFrigateCardCustomAction(
+ 'snapshots',
+ ) as FrigateCardCustomAction,
+ hold_action: createFrigateCardCustomAction(
+ 'snapshot',
+ ) as FrigateCardCustomAction,
+ });
}
+
if (this._getConfig().menu.buttons.image) {
- buttons.push(
- this._getFrigateCardMenuButton({
- tap_action: 'image',
- title: localize('config.view.views.image'),
- icon: 'mdi:image',
- emphasize: this._view?.is('image'),
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.view.views.image'),
+ icon: 'mdi:image',
+ style: this._view?.is('image') ? this._getEmphasizedStyle() : {},
+ tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
+ });
}
+
if (this._getConfig().menu.buttons.download && this._view?.isViewerView()) {
- buttons.push(
- this._getFrigateCardMenuButton({
- tap_action: 'download',
- title: localize('config.menu.buttons.download'),
- icon: 'mdi:download',
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.menu.buttons.download'),
+ icon: 'mdi:download',
+ tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
+ });
}
const cameraConfig = this._getSelectedCameraConfig();
@@ -381,22 +362,26 @@ export class FrigateCard extends LitElement {
cameraConfig &&
cameraConfig.frigate_url
) {
- buttons.push(
- this._getFrigateCardMenuButton({
- tap_action: 'frigate_ui',
- title: localize('config.menu.buttons.frigate_ui'),
- icon: 'mdi:web',
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.menu.buttons.frigate_ui'),
+ icon: 'mdi:web',
+ tap_action: createFrigateCardCustomAction(
+ 'frigate_ui',
+ ) as FrigateCardCustomAction,
+ });
}
+
if (this._getConfig().menu.buttons.fullscreen && screenfull.isEnabled) {
- buttons.push(
- this._getFrigateCardMenuButton({
- tap_action: 'fullscreen',
- title: localize('config.menu.buttons.fullscreen'),
- icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
- }),
- );
+ buttons.push({
+ type: 'custom:frigate-card-menu-icon',
+ title: localize('config.menu.buttons.fullscreen'),
+ icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
+ tap_action: createFrigateCardCustomAction(
+ 'fullscreen',
+ ) as FrigateCardCustomAction,
+ style: screenfull.isFullscreen ? this._getEmphasizedStyle() : {},
+ });
}
return buttons.concat(this._dynamicMenuButtons);
}
@@ -804,7 +789,7 @@ export class FrigateCard extends LitElement {
const action = frigateCardAction.frigate_card_action;
switch (action) {
- case 'frigate':
+ case 'default':
this._changeView();
break;
case 'clip':
@@ -874,16 +859,7 @@ export class FrigateCard extends LitElement {
* Handle an action called on an element.
* @param ev The actionHandler event.
*/
- protected _actionHandler(
- ev: CustomEvent,
- config?: {
- hold_action?: ActionType;
- tap_action?: ActionType;
- double_tap_action?: ActionType;
- start_tap_action?: ActionType;
- end_tap_action?: ActionType;
- },
- ): void {
+ protected _actionHandler(ev: CustomEvent, config?: Actions): void {
const interaction = ev.detail.action;
const node: HTMLElement | null = ev.currentTarget as HTMLElement | null;
if (
@@ -1143,8 +1119,8 @@ export class FrigateCard extends LitElement {
return html` this._actionHandler(ev, actions)}
@ll-custom=${this._cardActionHandler.bind(this)}
diff --git a/src/common.ts b/src/common.ts
index f12c463c..34e5511a 100644
--- a/src/common.ts
+++ b/src/common.ts
@@ -1,15 +1,13 @@
import { HassEntity, MessageBase } from 'home-assistant-js-websocket';
-import {
- HomeAssistant,
- handleActionConfig,
- stateIcon,
-} from 'custom-card-helpers';
+import { HomeAssistant, handleActionConfig, hasAction, stateIcon } from 'custom-card-helpers';
import { StyleInfo } from 'lit/directives/style-map';
import { ZodSchema, z } from 'zod';
import { isEqual } from 'lodash-es';
import { localize } from './localize/localize.js';
import {
+ Actions,
+ ActionsConfig,
ActionType,
CameraConfig,
ExtendedHomeAssistant,
@@ -302,14 +300,8 @@ export function createFrigateCardCustomAction(
*/
export function getActionConfigGivenAction(
interaction?: string,
- config?: {
- hold_action?: ActionType;
- tap_action?: ActionType;
- double_tap_action?: ActionType;
- start_tap_action?: ActionType;
- end_tap_action?: ActionType;
- },
-): ActionType | undefined {
+ config?: Actions,
+): ActionType | ActionType[] | undefined {
if (!interaction || !config) {
return undefined;
}
@@ -499,24 +491,63 @@ export function contentsChanged(n: unknown, o: unknown): boolean {
* @param hass The Home Assistant object.
* @param config The multi-action configuration.
* @param action The action string (e.g. 'hold')
+ * @returns Whether or not an action was executed.
*/
export const frigateCardHandleAction = (
+ node: HTMLElement,
+ hass: HomeAssistant,
+ config: ActionsConfig,
+ action: string,
+): boolean => {
+ return frigateCardHandleActionConfig(
+ node,
+ hass,
+ config,
+ action,
+ getActionConfigGivenAction(action, config),
+ );
+};
+
+/**
+ * Handle an ActionConfig or array of ActionConfigs.
+ * @param node The node that fired the event.
+ * @param hass The Home Assistant object.
+ * @param actionConfig A single action config, array of action configs or
+ * undefined for the default action config for 'tap'.
+ * @param action The action string (e.g. 'hold')
+ * @returns Whether or not an action was executed.
+ */
+export const frigateCardHandleActionConfig = (
node: HTMLElement,
hass: HomeAssistant,
config: {
- entity?: string;
camera_image?: string;
- hold_action?: ActionType;
- tap_action?: ActionType;
- double_tap_action?: ActionType;
- start_tap_action?: ActionType;
- end_tap_action?: ActionType;
+ entity?: string;
},
action: string,
-): void => {
- const actionConfig = getActionConfigGivenAction(action, config);
+ actionConfig: ActionType | ActionType[] | undefined,
+): boolean => {
if (actionConfig || action == 'tap') {
// Only allow a tap action to use a default non-config (the more-info config).
- handleActionConfig(node, hass, config, actionConfig);
+ if (Array.isArray(actionConfig)) {
+ actionConfig.forEach((action) => handleActionConfig(node, hass, config, action));
+ } else {
+ handleActionConfig(node, hass, config, actionConfig);
+ }
+ return true;
}
+ return false;
};
+
+/**
+ * Determine if an action config has a real action. A modified version of
+ * custom-card-helpers hasAction to also work with arrays of action configs.
+ * @param config The action config in question.
+ * @returns `true` if there's a real action defined, `false` otherwise.
+ */
+export const frigateCardHasAction = (config?: ActionType | ActionType[] | undefined): boolean => {
+ if (Array.isArray(config)) {
+ return !!config.find((item) => hasAction(item));
+ }
+ return hasAction(config);
+}
\ No newline at end of file
diff --git a/src/components/menu.ts b/src/components/menu.ts
index 737bfa38..f1d48453 100644
--- a/src/components/menu.ts
+++ b/src/components/menu.ts
@@ -1,4 +1,4 @@
-import { HASSDomEvent, HomeAssistant, hasAction } from 'custom-card-helpers';
+import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -9,7 +9,7 @@ import { actionHandler } from '../action-handler-directive.js';
import './submenu.js';
import type {
- Actions,
+ ActionsConfig,
ActionType,
ExtendedHomeAssistant,
MenuButton,
@@ -18,7 +18,8 @@ import type {
} from '../types.js';
import {
convertActionToFrigateCardCustomAction,
- frigateCardHandleAction,
+ frigateCardHandleActionConfig,
+ frigateCardHasAction,
getActionConfigGivenAction,
refreshDynamicStateParameters,
} from '../common.js';
@@ -37,8 +38,6 @@ export class FrigateCardMenu extends LitElement {
public hass?: HomeAssistant & ExtendedHomeAssistant;
set menuConfig(menuConfig: MenuConfig) {
- this.expanded = !menuConfig?.mode.startsWith('hidden-');
-
this._menuConfig = menuConfig;
if (menuConfig) {
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
@@ -53,11 +52,36 @@ export class FrigateCardMenu extends LitElement {
@property({ attribute: false })
public buttons: MenuButton[] = [];
- protected _isFrigateCardAction(action: ActionType): boolean {
+ /**
+ * Determine if a given menu configuration is a hiding menu.
+ * @param menuConfig The menu configuration.
+ * @returns `true` if the menu is hiding, `false` otherwise.
+ */
+ static isHidingMenu(menuConfig: MenuConfig | undefined): boolean {
+ return menuConfig?.mode.startsWith('hidden-') ?? false;
+ }
+
+ /**
+ * Determine if a given menu configuration is a hiding menu (internal version).
+ * @returns `true` if the menu is hiding, `false` otherwise.
+ */
+ protected _isHidingMenu(): boolean {
+ return FrigateCardMenu.isHidingMenu(this._menuConfig);
+ }
+
+ /**
+ * Determine if a given action is intended to toggle the menu.
+ * @param action The action to check.
+ * @returns `true` if the action toggles the menu, `false` otherwise.
+ */
+ protected _isMenuToggleAction(action: ActionType | undefined): boolean {
// Determine if this action is a Frigate card action, if so handle it
// internally.
+ if (!action) {
+ return false;
+ }
const frigateCardAction = convertActionToFrigateCardCustomAction(action);
- return !!frigateCardAction && frigateCardAction.frigate_card_action == 'frigate';
+ return !!frigateCardAction && frigateCardAction.frigate_card_action == 'menu_toggle';
}
/**
@@ -66,8 +90,8 @@ export class FrigateCardMenu extends LitElement {
* @param button The button configuration.
*/
protected _actionHandler(
- ev: HASSDomEvent<{ action: string; config?: Actions }>,
- config?: Actions,
+ ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
+ config?: ActionsConfig,
): void {
if (!ev) {
return;
@@ -86,25 +110,56 @@ export class FrigateCardMenu extends LitElement {
ev.stopPropagation();
const interaction: string = ev.detail.action;
- const action = getActionConfigGivenAction(interaction, config);
+ let action = getActionConfigGivenAction(interaction, config);
if (!config || !interaction) {
return;
}
- if (
- action &&
- this._isFrigateCardAction(action) &&
- 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.expanded = !this.expanded;
- return;
+ let tookAction = false;
+ let menuToggle = false;
+
+ if (Array.isArray(action)) {
+ // Case 1: An array of actions.
+ // Strip out actions that toggle the menu.
+ const actionCount = action.length;
+ action = action.filter((item) => !this._isMenuToggleAction(item));
+ if (action.length != actionCount) {
+ menuToggle = true;
+ }
+
+ // If there are still actions left, handle them as usual.
+ if (action.length) {
+ tookAction = frigateCardHandleActionConfig(
+ this,
+ this.hass as HomeAssistant,
+ config,
+ interaction,
+ action,
+ );
+ }
+ } else {
+ // Case 2: Either a specific action, or no action at all (i.e. default
+ // action for `tap`).
+ if (this._isMenuToggleAction(action)) {
+ menuToggle = true;
+ } else {
+ tookAction = frigateCardHandleActionConfig(
+ this,
+ this.hass as HomeAssistant,
+ config,
+ interaction,
+ action,
+ );
+ }
}
- // Collapse menu after the user clicks on something.
- this.expanded = false;
- frigateCardHandleAction(this, this.hass as HomeAssistant, config, interaction);
+ if (this._isHidingMenu()) {
+ if (menuToggle) {
+ this.expanded = !this.expanded;
+ } else if (tookAction) {
+ this.expanded = false;
+ }
+ }
}
/**
@@ -146,8 +201,8 @@ export class FrigateCardMenu extends LitElement {
stateParameters = refreshDynamicStateParameters(this.hass, stateParameters);
}
- const hasHold = hasAction(button.hold_action);
- const hasDoubleClick = hasAction(button.double_tap_action);
+ const hasHold = frigateCardHasAction(button.hold_action);
+ const hasDoubleClick = frigateCardHasAction(button.double_tap_action);
const classes = {
button: true,
diff --git a/src/components/submenu.ts b/src/components/submenu.ts
index fc45cbb9..b111d4db 100644
--- a/src/components/submenu.ts
+++ b/src/components/submenu.ts
@@ -1,11 +1,11 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators';
-import { hasAction, HomeAssistant } from 'custom-card-helpers';
+import { HomeAssistant } from 'custom-card-helpers';
import { styleMap } from 'lit/directives/style-map';
import { ExtendedHomeAssistant, MenuSubmenu, MenuSubmenuItem } from '../types.js';
import { actionHandler } from '../action-handler-directive.js';
-import { refreshDynamicStateParameters } from '../common.js';
+import { frigateCardHasAction, refreshDynamicStateParameters } from '../common.js';
import submenuStyle from '../scss/submenu.scss';
import type { Corner } from "@material/mwc-menu";
@@ -39,8 +39,8 @@ export class FrigateCardSubmenu extends LitElement {
ev.detail.config = item;
}}
.actionHandler=${actionHandler({
- hasHold: hasAction(item.hold_action),
- hasDoubleClick: hasAction(item.double_tap_action),
+ hasHold: frigateCardHasAction(item.hold_action),
+ hasDoubleClick: frigateCardHasAction(item.double_tap_action),
})}
>
${stateParameters.title || ''}
@@ -71,8 +71,8 @@ export class FrigateCardSubmenu extends LitElement {
slot="trigger"
.label=${this.submenu.title || ''}
.actionHandler=${actionHandler({
- hasHold: hasAction(this.submenu.hold_action),
- hasDoubleClick: hasAction(this.submenu.double_tap_action),
+ hasHold: frigateCardHasAction(this.submenu.hold_action),
+ hasDoubleClick: frigateCardHasAction(this.submenu.double_tap_action),
})}
>
diff --git a/src/types.ts b/src/types.ts
index 060017df..7c6d7aa0 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -119,7 +119,7 @@ const frigateCardCustomActionBaseSchema = customActionSchema.extend({
});
const FRIGATE_CARD_GENERAL_ACTIONS = [
- 'frigate',
+ 'default',
'clip',
'clips',
'image',
@@ -129,6 +129,7 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
'download',
'frigate_ui',
'fullscreen',
+ 'menu_toggle',
] as const;
const FRIGATE_CARD_ACTIONS = [...FRIGATE_CARD_GENERAL_ACTIONS, 'camera_select'] as const;
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
@@ -159,17 +160,21 @@ export type ActionType = z.infer;
const actionBaseSchema = z
.object({
- tap_action: actionSchema.optional(),
- hold_action: actionSchema.optional(),
- double_tap_action: actionSchema.optional(),
- start_tap_action: actionSchema.optional(),
- end_tap_action: actionSchema.optional(),
+ tap_action: actionSchema.or(actionSchema.array()).optional(),
+ hold_action: actionSchema.or(actionSchema.array()).optional(),
+ double_tap_action: actionSchema.or(actionSchema.array()).optional(),
+ start_tap_action: actionSchema.or(actionSchema.array()).optional(),
+ end_tap_action: actionSchema.or(actionSchema.array()).optional(),
})
// 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.
.passthrough();
export type Actions = z.infer;
+export type ActionsConfig = Actions & {
+ camera_image?: string;
+ entity?: string;
+};
const actionsSchema = z.object({
actions: actionBaseSchema.optional(),
@@ -746,14 +751,6 @@ export interface BrowseMediaQueryParameters {
after?: number;
}
-export interface GetFrigateCardMenuButtonParameters {
- icon: string;
- title: string;
- tap_action: FrigateCardAction;
- hold_action?: FrigateCardAction;
- emphasize?: boolean;
-}
-
export interface BrowseMediaNeighbors {
previous: BrowseMediaSource | null;
previousIndex: number | null;