Allow Frigate card actions to be a list of actions.
This commit is contained in:
@@ -497,6 +497,10 @@ See the [action
|
|||||||
documentation](https://www.home-assistant.io/lovelace/actions/#hold-action) for
|
documentation](https://www.home-assistant.io/lovelace/actions/#hold-action) for
|
||||||
more information on the action options available.
|
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
|
### 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.
|
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 |
|
| 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 |
|
| 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).|
|
| `clip`, `clips`, `image`, `live`, `snapshot`, `snapshots` | Trigger the named [view](#views).|
|
||||||
|`download`|Download the displayed media.|
|
|`download`|Download the displayed media.|
|
||||||
|`frigate_ui`|Open the Frigate UI at the configured URL.|
|
|`frigate_ui`|Open the Frigate UI at the configured URL.|
|
||||||
|`fullscreen`|Toggle fullscreen.|
|
|`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.|
|
|`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). |
|
||||||
|
|
||||||
<a name="views"></a>
|
<a name="views"></a>
|
||||||
|
|
||||||
@@ -1111,6 +1116,29 @@ image:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<a name="example-multiple-actions"></a>
|
||||||
|
|
||||||
|
### Defining multiple actions for Elements
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Changing camera and view simultaneously</summary>
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
<a name="card-updates"></a>
|
<a name="card-updates"></a>
|
||||||
|
|
||||||
## Card Refreshes
|
## Card Refreshes
|
||||||
|
|||||||
+78
-102
@@ -14,7 +14,6 @@ import {
|
|||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
LovelaceCardEditor,
|
LovelaceCardEditor,
|
||||||
getLovelace,
|
getLovelace,
|
||||||
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';
|
||||||
@@ -23,10 +22,10 @@ import {
|
|||||||
Actions,
|
Actions,
|
||||||
ActionType,
|
ActionType,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
GetFrigateCardMenuButtonParameters,
|
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
entitySchema,
|
entitySchema,
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
|
FrigateCardCustomAction,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import type {
|
import type {
|
||||||
Entity,
|
Entity,
|
||||||
@@ -46,6 +45,7 @@ import {
|
|||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
createFrigateCardCustomAction,
|
createFrigateCardCustomAction,
|
||||||
frigateCardHandleAction,
|
frigateCardHandleAction,
|
||||||
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
getCameraIcon,
|
getCameraIcon,
|
||||||
getCameraTitle,
|
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.
|
* Get the menu buttons to display.
|
||||||
* @returns An array of menu buttons.
|
* @returns An array of menu buttons.
|
||||||
@@ -286,16 +264,17 @@ export class FrigateCard extends LitElement {
|
|||||||
const buttons: MenuButton[] = [];
|
const buttons: MenuButton[] = [];
|
||||||
|
|
||||||
if (this._getConfig().menu.buttons.frigate) {
|
if (this._getConfig().menu.buttons.frigate) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'frigate',
|
title: localize('config.menu.buttons.frigate'),
|
||||||
// Use a magic icon value that the menu will use to render the icon as
|
// Use a magic icon value that the menu will use to render the icon as
|
||||||
// it deems appropriate (certain menu configurations change the menu
|
// it deems appropriate (certain menu configurations change the menu
|
||||||
// icon for the 'Frigate' button).
|
// icon for the 'Frigate' button).
|
||||||
icon: FRIGATE_BUTTON_MENU_ICON,
|
icon: FRIGATE_BUTTON_MENU_ICON,
|
||||||
title: localize('config.menu.buttons.frigate'),
|
tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu)
|
||||||
}),
|
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
|
||||||
);
|
: (createFrigateCardCustomAction('default') as FrigateCardCustomAction),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -323,56 +302,58 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._getConfig().menu.buttons.live) {
|
if (this._getConfig().menu.buttons.live) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'live',
|
title: localize('config.view.views.live'),
|
||||||
title: localize('config.view.views.live'),
|
icon: 'mdi:cctv',
|
||||||
icon: 'mdi:cctv',
|
style: this._view?.is('live') ? this._getEmphasizedStyle() : {},
|
||||||
emphasize: this._view?.is('live'),
|
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._getConfig().menu.buttons.clips) {
|
if (this._getConfig().menu.buttons.clips) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'clips',
|
title: localize('config.view.views.clips'),
|
||||||
hold_action: 'clip',
|
icon: 'mdi:filmstrip',
|
||||||
title: localize('config.view.views.clips'),
|
style: this._view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||||
icon: 'mdi:filmstrip',
|
tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
|
||||||
emphasize: this._view?.is('clips'),
|
hold_action: createFrigateCardCustomAction('clip') as FrigateCardCustomAction,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._getConfig().menu.buttons.snapshots) {
|
if (this._getConfig().menu.buttons.snapshots) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'snapshots',
|
title: localize('config.view.views.snapshots'),
|
||||||
hold_action: 'snapshot',
|
icon: 'mdi:camera',
|
||||||
title: localize('config.view.views.snapshots'),
|
style: this._view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||||
icon: 'mdi:camera',
|
tap_action: createFrigateCardCustomAction(
|
||||||
emphasize: this._view?.is('snapshots'),
|
'snapshots',
|
||||||
}),
|
) as FrigateCardCustomAction,
|
||||||
);
|
hold_action: createFrigateCardCustomAction(
|
||||||
|
'snapshot',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._getConfig().menu.buttons.image) {
|
if (this._getConfig().menu.buttons.image) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'image',
|
title: localize('config.view.views.image'),
|
||||||
title: localize('config.view.views.image'),
|
icon: 'mdi:image',
|
||||||
icon: 'mdi:image',
|
style: this._view?.is('image') ? this._getEmphasizedStyle() : {},
|
||||||
emphasize: this._view?.is('image'),
|
tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._getConfig().menu.buttons.download && this._view?.isViewerView()) {
|
if (this._getConfig().menu.buttons.download && this._view?.isViewerView()) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'download',
|
title: localize('config.menu.buttons.download'),
|
||||||
title: localize('config.menu.buttons.download'),
|
icon: 'mdi:download',
|
||||||
icon: 'mdi:download',
|
tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cameraConfig = this._getSelectedCameraConfig();
|
const cameraConfig = this._getSelectedCameraConfig();
|
||||||
@@ -381,22 +362,26 @@ export class FrigateCard extends LitElement {
|
|||||||
cameraConfig &&
|
cameraConfig &&
|
||||||
cameraConfig.frigate_url
|
cameraConfig.frigate_url
|
||||||
) {
|
) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'frigate_ui',
|
title: localize('config.menu.buttons.frigate_ui'),
|
||||||
title: localize('config.menu.buttons.frigate_ui'),
|
icon: 'mdi:web',
|
||||||
icon: 'mdi:web',
|
tap_action: createFrigateCardCustomAction(
|
||||||
}),
|
'frigate_ui',
|
||||||
);
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._getConfig().menu.buttons.fullscreen && screenfull.isEnabled) {
|
if (this._getConfig().menu.buttons.fullscreen && screenfull.isEnabled) {
|
||||||
buttons.push(
|
buttons.push({
|
||||||
this._getFrigateCardMenuButton({
|
type: 'custom:frigate-card-menu-icon',
|
||||||
tap_action: 'fullscreen',
|
title: localize('config.menu.buttons.fullscreen'),
|
||||||
title: localize('config.menu.buttons.fullscreen'),
|
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi: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);
|
return buttons.concat(this._dynamicMenuButtons);
|
||||||
}
|
}
|
||||||
@@ -804,7 +789,7 @@ export class FrigateCard extends LitElement {
|
|||||||
const action = frigateCardAction.frigate_card_action;
|
const action = frigateCardAction.frigate_card_action;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'frigate':
|
case 'default':
|
||||||
this._changeView();
|
this._changeView();
|
||||||
break;
|
break;
|
||||||
case 'clip':
|
case 'clip':
|
||||||
@@ -874,16 +859,7 @@ export class FrigateCard extends LitElement {
|
|||||||
* Handle an action called on an element.
|
* Handle an action called on an element.
|
||||||
* @param ev The actionHandler event.
|
* @param ev The actionHandler event.
|
||||||
*/
|
*/
|
||||||
protected _actionHandler(
|
protected _actionHandler(ev: CustomEvent, config?: Actions): void {
|
||||||
ev: CustomEvent,
|
|
||||||
config?: {
|
|
||||||
hold_action?: ActionType;
|
|
||||||
tap_action?: ActionType;
|
|
||||||
double_tap_action?: ActionType;
|
|
||||||
start_tap_action?: ActionType;
|
|
||||||
end_tap_action?: ActionType;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
const interaction = ev.detail.action;
|
const interaction = ev.detail.action;
|
||||||
const node: HTMLElement | null = ev.currentTarget as HTMLElement | null;
|
const node: HTMLElement | null = ev.currentTarget as HTMLElement | null;
|
||||||
if (
|
if (
|
||||||
@@ -1143,8 +1119,8 @@ export class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
return html` <ha-card
|
return html` <ha-card
|
||||||
.actionHandler=${actionHandler({
|
.actionHandler=${actionHandler({
|
||||||
hasHold: hasAction(actions.hold_action),
|
hasHold: frigateCardHasAction(actions.hold_action),
|
||||||
hasDoubleClick: hasAction(actions.double_tap_action),
|
hasDoubleClick: frigateCardHasAction(actions.double_tap_action),
|
||||||
})}
|
})}
|
||||||
@action=${(ev: CustomEvent) => this._actionHandler(ev, actions)}
|
@action=${(ev: CustomEvent) => this._actionHandler(ev, actions)}
|
||||||
@ll-custom=${this._cardActionHandler.bind(this)}
|
@ll-custom=${this._cardActionHandler.bind(this)}
|
||||||
|
|||||||
+53
-22
@@ -1,15 +1,13 @@
|
|||||||
import { HassEntity, MessageBase } from 'home-assistant-js-websocket';
|
import { HassEntity, MessageBase } from 'home-assistant-js-websocket';
|
||||||
import {
|
import { HomeAssistant, handleActionConfig, hasAction, stateIcon } from 'custom-card-helpers';
|
||||||
HomeAssistant,
|
|
||||||
handleActionConfig,
|
|
||||||
stateIcon,
|
|
||||||
} from 'custom-card-helpers';
|
|
||||||
import { StyleInfo } from 'lit/directives/style-map';
|
import { StyleInfo } from 'lit/directives/style-map';
|
||||||
import { ZodSchema, z } from 'zod';
|
import { ZodSchema, z } from 'zod';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
import {
|
import {
|
||||||
|
Actions,
|
||||||
|
ActionsConfig,
|
||||||
ActionType,
|
ActionType,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
@@ -302,14 +300,8 @@ export function createFrigateCardCustomAction(
|
|||||||
*/
|
*/
|
||||||
export function getActionConfigGivenAction(
|
export function getActionConfigGivenAction(
|
||||||
interaction?: string,
|
interaction?: string,
|
||||||
config?: {
|
config?: Actions,
|
||||||
hold_action?: ActionType;
|
): ActionType | ActionType[] | undefined {
|
||||||
tap_action?: ActionType;
|
|
||||||
double_tap_action?: ActionType;
|
|
||||||
start_tap_action?: ActionType;
|
|
||||||
end_tap_action?: ActionType;
|
|
||||||
},
|
|
||||||
): ActionType | undefined {
|
|
||||||
if (!interaction || !config) {
|
if (!interaction || !config) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -499,24 +491,63 @@ export function contentsChanged(n: unknown, o: unknown): boolean {
|
|||||||
* @param hass The Home Assistant object.
|
* @param hass The Home Assistant object.
|
||||||
* @param config The multi-action configuration.
|
* @param config The multi-action configuration.
|
||||||
* @param action The action string (e.g. 'hold')
|
* @param action The action string (e.g. 'hold')
|
||||||
|
* @returns Whether or not an action was executed.
|
||||||
*/
|
*/
|
||||||
export const frigateCardHandleAction = (
|
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,
|
node: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: {
|
config: {
|
||||||
entity?: string;
|
|
||||||
camera_image?: string;
|
camera_image?: string;
|
||||||
hold_action?: ActionType;
|
entity?: string;
|
||||||
tap_action?: ActionType;
|
|
||||||
double_tap_action?: ActionType;
|
|
||||||
start_tap_action?: ActionType;
|
|
||||||
end_tap_action?: ActionType;
|
|
||||||
},
|
},
|
||||||
action: string,
|
action: string,
|
||||||
): void => {
|
actionConfig: ActionType | ActionType[] | undefined,
|
||||||
const actionConfig = getActionConfigGivenAction(action, config);
|
): boolean => {
|
||||||
if (actionConfig || action == 'tap') {
|
if (actionConfig || action == 'tap') {
|
||||||
// Only allow a tap action to use a default non-config (the more-info config).
|
// 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);
|
||||||
|
}
|
||||||
+79
-24
@@ -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 { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -9,7 +9,7 @@ import { actionHandler } from '../action-handler-directive.js';
|
|||||||
import './submenu.js';
|
import './submenu.js';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Actions,
|
ActionsConfig,
|
||||||
ActionType,
|
ActionType,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
@@ -18,7 +18,8 @@ import type {
|
|||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
frigateCardHandleAction,
|
frigateCardHandleActionConfig,
|
||||||
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
refreshDynamicStateParameters,
|
refreshDynamicStateParameters,
|
||||||
} from '../common.js';
|
} from '../common.js';
|
||||||
@@ -37,8 +38,6 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
public hass?: HomeAssistant & ExtendedHomeAssistant;
|
public hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
set menuConfig(menuConfig: MenuConfig) {
|
set menuConfig(menuConfig: MenuConfig) {
|
||||||
this.expanded = !menuConfig?.mode.startsWith('hidden-');
|
|
||||||
|
|
||||||
this._menuConfig = menuConfig;
|
this._menuConfig = menuConfig;
|
||||||
if (menuConfig) {
|
if (menuConfig) {
|
||||||
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
|
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
|
||||||
@@ -53,11 +52,36 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public buttons: MenuButton[] = [];
|
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
|
// Determine if this action is a Frigate card action, if so handle it
|
||||||
// internally.
|
// internally.
|
||||||
|
if (!action) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const frigateCardAction = convertActionToFrigateCardCustomAction(action);
|
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.
|
* @param button The button configuration.
|
||||||
*/
|
*/
|
||||||
protected _actionHandler(
|
protected _actionHandler(
|
||||||
ev: HASSDomEvent<{ action: string; config?: Actions }>,
|
ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
|
||||||
config?: Actions,
|
config?: ActionsConfig,
|
||||||
): void {
|
): void {
|
||||||
if (!ev) {
|
if (!ev) {
|
||||||
return;
|
return;
|
||||||
@@ -86,25 +110,56 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
const interaction: string = ev.detail.action;
|
const interaction: string = ev.detail.action;
|
||||||
const action = getActionConfigGivenAction(interaction, config);
|
let action = getActionConfigGivenAction(interaction, config);
|
||||||
if (!config || !interaction) {
|
if (!config || !interaction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
let tookAction = false;
|
||||||
action &&
|
let menuToggle = false;
|
||||||
this._isFrigateCardAction(action) &&
|
|
||||||
this._menuConfig?.mode.startsWith('hidden-')
|
if (Array.isArray(action)) {
|
||||||
) {
|
// Case 1: An array of actions.
|
||||||
// If the user presses the frigate button and it's a hide-away menu,
|
// Strip out actions that toggle the menu.
|
||||||
// then expand the menu and return.
|
const actionCount = action.length;
|
||||||
this.expanded = !this.expanded;
|
action = action.filter((item) => !this._isMenuToggleAction(item));
|
||||||
return;
|
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.
|
if (this._isHidingMenu()) {
|
||||||
this.expanded = false;
|
if (menuToggle) {
|
||||||
frigateCardHandleAction(this, this.hass as HomeAssistant, config, interaction);
|
this.expanded = !this.expanded;
|
||||||
|
} else if (tookAction) {
|
||||||
|
this.expanded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,8 +201,8 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
stateParameters = refreshDynamicStateParameters(this.hass, stateParameters);
|
stateParameters = refreshDynamicStateParameters(this.hass, stateParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasHold = hasAction(button.hold_action);
|
const hasHold = frigateCardHasAction(button.hold_action);
|
||||||
const hasDoubleClick = hasAction(button.double_tap_action);
|
const hasDoubleClick = frigateCardHasAction(button.double_tap_action);
|
||||||
|
|
||||||
const classes = {
|
const classes = {
|
||||||
button: true,
|
button: true,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators';
|
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 { styleMap } from 'lit/directives/style-map';
|
||||||
|
|
||||||
import { ExtendedHomeAssistant, MenuSubmenu, MenuSubmenuItem } from '../types.js';
|
import { ExtendedHomeAssistant, MenuSubmenu, MenuSubmenuItem } from '../types.js';
|
||||||
import { actionHandler } from '../action-handler-directive.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 submenuStyle from '../scss/submenu.scss';
|
||||||
import type { Corner } from "@material/mwc-menu";
|
import type { Corner } from "@material/mwc-menu";
|
||||||
@@ -39,8 +39,8 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
ev.detail.config = item;
|
ev.detail.config = item;
|
||||||
}}
|
}}
|
||||||
.actionHandler=${actionHandler({
|
.actionHandler=${actionHandler({
|
||||||
hasHold: hasAction(item.hold_action),
|
hasHold: frigateCardHasAction(item.hold_action),
|
||||||
hasDoubleClick: hasAction(item.double_tap_action),
|
hasDoubleClick: frigateCardHasAction(item.double_tap_action),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${stateParameters.title || ''}
|
${stateParameters.title || ''}
|
||||||
@@ -71,8 +71,8 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.submenu.title || ''}
|
.label=${this.submenu.title || ''}
|
||||||
.actionHandler=${actionHandler({
|
.actionHandler=${actionHandler({
|
||||||
hasHold: hasAction(this.submenu.hold_action),
|
hasHold: frigateCardHasAction(this.submenu.hold_action),
|
||||||
hasDoubleClick: hasAction(this.submenu.double_tap_action),
|
hasDoubleClick: frigateCardHasAction(this.submenu.double_tap_action),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ha-icon icon="${this.submenu.icon}"></ha-icon>
|
<ha-icon icon="${this.submenu.icon}"></ha-icon>
|
||||||
|
|||||||
+11
-14
@@ -119,7 +119,7 @@ const frigateCardCustomActionBaseSchema = customActionSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const FRIGATE_CARD_GENERAL_ACTIONS = [
|
const FRIGATE_CARD_GENERAL_ACTIONS = [
|
||||||
'frigate',
|
'default',
|
||||||
'clip',
|
'clip',
|
||||||
'clips',
|
'clips',
|
||||||
'image',
|
'image',
|
||||||
@@ -129,6 +129,7 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
|||||||
'download',
|
'download',
|
||||||
'frigate_ui',
|
'frigate_ui',
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
|
'menu_toggle',
|
||||||
] as const;
|
] as const;
|
||||||
const FRIGATE_CARD_ACTIONS = [...FRIGATE_CARD_GENERAL_ACTIONS, 'camera_select'] as const;
|
const FRIGATE_CARD_ACTIONS = [...FRIGATE_CARD_GENERAL_ACTIONS, 'camera_select'] as const;
|
||||||
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
|
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
|
||||||
@@ -159,17 +160,21 @@ export type ActionType = z.infer<typeof actionSchema>;
|
|||||||
|
|
||||||
const actionBaseSchema = z
|
const actionBaseSchema = z
|
||||||
.object({
|
.object({
|
||||||
tap_action: actionSchema.optional(),
|
tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||||
hold_action: actionSchema.optional(),
|
hold_action: actionSchema.or(actionSchema.array()).optional(),
|
||||||
double_tap_action: actionSchema.optional(),
|
double_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||||
start_tap_action: actionSchema.optional(),
|
start_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||||
end_tap_action: actionSchema.optional(),
|
end_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||||
})
|
})
|
||||||
// Passthrough to allow (at least) entity/camera_image to go through. This
|
// Passthrough to allow (at least) entity/camera_image to go through. This
|
||||||
// card doesn't need these attributes, but handleAction() in
|
// card doesn't need these attributes, but handleAction() in
|
||||||
// custom_card_helpers may depending on how the action is configured.
|
// custom_card_helpers may depending on how the action is configured.
|
||||||
.passthrough();
|
.passthrough();
|
||||||
export type Actions = z.infer<typeof actionBaseSchema>;
|
export type Actions = z.infer<typeof actionBaseSchema>;
|
||||||
|
export type ActionsConfig = Actions & {
|
||||||
|
camera_image?: string;
|
||||||
|
entity?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const actionsSchema = z.object({
|
const actionsSchema = z.object({
|
||||||
actions: actionBaseSchema.optional(),
|
actions: actionBaseSchema.optional(),
|
||||||
@@ -746,14 +751,6 @@ export interface BrowseMediaQueryParameters {
|
|||||||
after?: number;
|
after?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetFrigateCardMenuButtonParameters {
|
|
||||||
icon: string;
|
|
||||||
title: string;
|
|
||||||
tap_action: FrigateCardAction;
|
|
||||||
hold_action?: FrigateCardAction;
|
|
||||||
emphasize?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BrowseMediaNeighbors {
|
export interface BrowseMediaNeighbors {
|
||||||
previous: BrowseMediaSource | null;
|
previous: BrowseMediaSource | null;
|
||||||
previousIndex: number | null;
|
previousIndex: number | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user