Merge pull request #286 from dermotduffy/on-release-action

Implement support for {start,end}_tap actions
This commit is contained in:
Dermot Duffy
2022-01-26 20:35:36 -08:00
committed by GitHub
6 changed files with 87 additions and 24 deletions
+18 -4
View File
@@ -529,7 +529,7 @@ Parameters for the `custom:frigate-card-menu-submenu` element are identical to t
| `state_color` | `true` | Whether or not the title and icon should be stylized based on state. | | `state_color` | `true` | Whether or not the title and icon should be stylized based on state. |
| `selected` | `false` | Whether or not to show this item as selected. | | `selected` | `false` | Whether or not to show this item as selected. |
| `style` | | Position and style the element using CSS. | | `style` | | Position and style the element using CSS. |
| `tap_action`, `double_tap_action` or `hold_action` | | Standard [Home Assistant action configuration](https://www.home-assistant.io/lovelace/actions). | | `tap_action`, `double_tap_action`, `hold_action`, `start_tap`, `end_tap` | | Standard [Home Assistant action configuration](https://www.home-assistant.io/lovelace/actions). |
See the [Configuring a Submenu example](#configuring-a-submenu-example). See the [Configuring a Submenu example](#configuring-a-submenu-example).
@@ -587,8 +587,9 @@ time as the snapshot (if any).
## Card & View Actions ## Card & View Actions
Actions may be attached to the card itself, to trigger action when the card Actions may be attached to the card itself, to trigger action when the card
experiences a `tap`, `double_tap` or `hold` event. These actions can be experiences a `tap`, `double_tap`, `hold`, `start_tap` or `end_tap` event. These
specified both for the overall card and for individual groups of view. actions can be specified both for the overall card and for individual groups of
view.
| Configuration path | Views to which it refers | | Configuration path | Views to which it refers |
| - | - | | - | - |
@@ -614,6 +615,19 @@ _also_, which means that a card-wide `tap` action probably isn't that useful as
it may be disorienting to the user and will trigger on all kinds of basic it may be disorienting to the user and will trigger on all kinds of basic
interaction on the card (e.g. tapping/clicking a menu button). interaction on the card (e.g. tapping/clicking a menu button).
### Special Custom Action Types: `start_tap` and `end_tap`
The card has partial support for two special action types `start_tap` and
`end_tap` which occur when a tap is started (e.g. mouse is pressed down /
touch begins), and ended (e.g. mouse released / touch ends) respectively. This
might be useful for PTZ cameras cameras to start/stop movement on touch.
**Caveats**: This support is only partial. Stock [Home Assistant picture
elements](https://www.home-assistant.io/lovelace/picture-elements/) do not
support these actions when rendered onto the card, but Frigate card controls
(e.g. card/view actions as described above, menu icons and submenus) do support
them by default. Network latency may introduce unavoidable imprecision between
`end_tap` and action actually occurring.
## Menu Modes ## Menu Modes
@@ -927,7 +941,7 @@ elements:
### Adding Card-wide Actions ### Adding Card-wide Actions
You can add actions to the card to be trigger on `tap`, `double_tap` or `hold`. See [actions](#actions) above. You can add actions to the card to be trigger on `tap`, `double_tap`, `hold`, `start_tap` or `end_tap`. See [actions](#actions) above.
<details> <details>
<summary>Expand: Adding a card-wide action</summary> <summary>Expand: Adding a card-wide action</summary>
+5
View File
@@ -117,6 +117,8 @@ class ActionHandler extends HTMLElement implements ActionHandler {
this.held = true; this.held = true;
}, this.holdTime); }, this.holdTime);
} }
fireEvent(element, 'action', { action: 'start_tap' });
}; };
const end = (ev: Event): void => { const end = (ev: Event): void => {
@@ -133,6 +135,9 @@ class ActionHandler extends HTMLElement implements ActionHandler {
this.stopAnimation(); this.stopAnimation();
this.timer = undefined; this.timer = undefined;
} }
fireEvent(element, 'action', { action: 'end_tap' });
if (options?.hasHold && this.held) { if (options?.hasHold && this.held) {
fireEvent(element, 'action', { action: 'hold' }); fireEvent(element, 'action', { action: 'hold' });
} else if (options?.hasDoubleClick) { } else if (options?.hasDoubleClick) {
+15 -10
View File
@@ -14,7 +14,6 @@ import {
HomeAssistant, HomeAssistant,
LovelaceCardEditor, LovelaceCardEditor,
getLovelace, getLovelace,
handleAction,
hasAction, hasAction,
} from 'custom-card-helpers'; } from 'custom-card-helpers';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
@@ -46,6 +45,7 @@ import {
contentsChanged, contentsChanged,
convertActionToFrigateCardCustomAction, convertActionToFrigateCardCustomAction,
createFrigateCardCustomAction, createFrigateCardCustomAction,
frigateCardHandleAction,
getActionConfigGivenAction, getActionConfigGivenAction,
getCameraIcon, getCameraIcon,
getCameraTitle, getCameraTitle,
@@ -102,8 +102,8 @@ import {
* *
* The card supports actions being configured in a number of places (e.g. tap on an * The card supports actions being configured in a number of places (e.g. tap on an
* element, double_tap on a menu item, hold on the live view). These actions are * element, double_tap on a menu item, hold on the live view). These actions are
* handled by handleAction() from custom-card-helpers. For Frigate-card specific * handled frigateCardHandleAction(). For Frigate-card specific actions,
* actions, handleAction() call will result in an ll-custom DOM event being * frigateCardHandleAction() call will result in an ll-custom DOM event being
* fired, which needs to be caught at the card level to handle. * fired, which needs to be caught at the card level to handle.
*/ */
@@ -880,6 +880,8 @@ export class FrigateCard extends LitElement {
hold_action?: ActionType; hold_action?: ActionType;
tap_action?: ActionType; tap_action?: ActionType;
double_tap_action?: ActionType; double_tap_action?: ActionType;
start_tap_action?: ActionType;
end_tap_action?: ActionType;
}, },
): void { ): void {
const interaction = ev.detail.action; const interaction = ev.detail.action;
@@ -888,12 +890,17 @@ export class FrigateCard extends LitElement {
config && config &&
node && node &&
interaction && interaction &&
// Don't call handleAction() unless there is explicitly an action defined // Don't call frigateCardHandleAction() unless there is explicitly an
// (as it uses a default that is unhelpful for views that have default // action defined (as it uses a default that is unhelpful for views that
// tap/click actions). // have default tap/click actions).
getActionConfigGivenAction(interaction, config) getActionConfigGivenAction(interaction, config)
) { ) {
handleAction(node, this._hass as HomeAssistant, config, ev.detail.action); frigateCardHandleAction(
node,
this._hass as HomeAssistant,
config,
ev.detail.action,
);
} }
// Set the 'screensaver' timer. // Set the 'screensaver' timer.
@@ -950,9 +957,7 @@ export class FrigateCard extends LitElement {
* @returns `true` if it's allowed, `false` otherwise. * @returns `true` if it's allowed, `false` otherwise.
*/ */
protected _isAutomatedViewUpdateAllowed(): boolean { protected _isAutomatedViewUpdateAllowed(): boolean {
return ( return this._getConfig().view.update_force || !this._interactionTimerID;
this._getConfig().view.update_force || !this._interactionTimerID
);
} }
/** /**
+40 -4
View File
@@ -1,5 +1,9 @@
import { HassEntity, MessageBase } from 'home-assistant-js-websocket'; import { HassEntity, MessageBase } from 'home-assistant-js-websocket';
import { HomeAssistant, stateIcon } from 'custom-card-helpers'; import {
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';
@@ -302,10 +306,12 @@ export function getActionConfigGivenAction(
hold_action?: ActionType; hold_action?: ActionType;
tap_action?: ActionType; tap_action?: ActionType;
double_tap_action?: ActionType; double_tap_action?: ActionType;
start_tap_action?: ActionType;
end_tap_action?: ActionType;
}, },
): ActionType | null { ): ActionType | undefined {
if (!interaction || !config) { if (!interaction || !config) {
return null; return undefined;
} }
if (interaction == 'tap' && config.tap_action) { if (interaction == 'tap' && config.tap_action) {
return config.tap_action; return config.tap_action;
@@ -313,8 +319,12 @@ export function getActionConfigGivenAction(
return config.hold_action; return config.hold_action;
} else if (interaction == 'double_tap' && config.double_tap_action) { } else if (interaction == 'double_tap' && config.double_tap_action) {
return config.double_tap_action; return config.double_tap_action;
} else if (interaction == 'end_tap' && config.end_tap_action) {
return config.end_tap_action;
} else if (interaction == 'start_tap' && config.start_tap_action) {
return config.start_tap_action;
} }
return null; return undefined;
} }
/** /**
@@ -480,3 +490,29 @@ export function arrayMove(target: unknown[], from: number, to: number): void {
export function contentsChanged(n: unknown, o: unknown): boolean { export function contentsChanged(n: unknown, o: unknown): boolean {
return !isEqual(n, o); return !isEqual(n, o);
} }
/**
* Frigate card custom version of handleAction
* (https://github.com/custom-cards/custom-card-helpers/blob/master/src/handle-action.ts)
* that handles the custom action events the card supports.
* @param node The node that fired the event.
* @param hass The Home Assistant object.
* @param config The multi-action configuration.
* @param action The action string (e.g. 'hold')
*/
export const frigateCardHandleAction = (
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;
},
action: string,
): void => {
handleActionConfig(node, hass, config, getActionConfigGivenAction(action, config));
};
+3 -2
View File
@@ -1,4 +1,4 @@
import { HASSDomEvent, HomeAssistant, handleAction, hasAction } from 'custom-card-helpers'; import { HASSDomEvent, HomeAssistant, hasAction } 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';
@@ -18,6 +18,7 @@ import type {
} from '../types.js'; } from '../types.js';
import { import {
convertActionToFrigateCardCustomAction, convertActionToFrigateCardCustomAction,
frigateCardHandleAction,
getActionConfigGivenAction, getActionConfigGivenAction,
refreshDynamicStateParameters, refreshDynamicStateParameters,
} from '../common.js'; } from '../common.js';
@@ -103,7 +104,7 @@ export class FrigateCardMenu extends LitElement {
// Collapse menu after the user clicks on something. // Collapse menu after the user clicks on something.
this.expanded = false; this.expanded = false;
handleAction(this, this.hass as HomeAssistant, config, interaction); frigateCardHandleAction(this, this.hass as HomeAssistant, config, interaction);
} }
/** /**
+5 -3
View File
@@ -162,14 +162,16 @@ const actionBaseSchema = z
tap_action: actionSchema.optional(), tap_action: actionSchema.optional(),
hold_action: actionSchema.optional(), hold_action: actionSchema.optional(),
double_tap_action: actionSchema.optional(), double_tap_action: actionSchema.optional(),
start_tap_action: actionSchema.optional(),
end_tap_action: actionSchema.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(); .passthrough();
export type Actions = z.infer<typeof actionBaseSchema>; export type Actions = z.infer<typeof actionBaseSchema>;
const actionsSchema = z.object({ 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(), actions: actionBaseSchema.optional(),
}); });