Add support for card and view actions.
This commit is contained in:
@@ -91,6 +91,7 @@ All variables listed are under a `view:` section.
|
|||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `default` | `live` | The view to show in the card by default. See [views](#views) below.|
|
| `default` | `live` | The view to show in the card by default. See [views](#views) below.|
|
||||||
| `timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
| `timeout` | | A numbers of seconds of inactivity after which the card will reset to the default configured view. Inactivity is defined as lack of interaction with the Frigate menu.|
|
||||||
|
| `actions` | | Actions to use for all views, individual actions may be overriden by view-specific actions. See [actions](#actions) below.|
|
||||||
|
|
||||||
### Menu options
|
### Menu options
|
||||||
|
|
||||||
@@ -113,6 +114,7 @@ All variables listed are under a `live:` section.
|
|||||||
| `webrtc.url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).|
|
| `webrtc.url` | | The RTSP url to pass to WebRTC. Specify this OR `webrtc.entity` (below).|
|
||||||
| `webrtc.entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). |
|
| `webrtc.entity` | | The RTSP entity to pass WebRTC. Specify this OR `webrtc.url` (above). |
|
||||||
| `webrtc.*`| | Any other options in a `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.|
|
| `webrtc.*`| | Any other options in a `webrtc:` YAML dictionary are silently passed through to WebRTC. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides.|
|
||||||
|
| `actions` | | Actions to use for the `live` view. See [actions](#actions) below.|
|
||||||
|
|
||||||
#### Available Live Providers
|
#### Available Live Providers
|
||||||
|
|
||||||
@@ -124,7 +126,7 @@ All variables listed are under a `live:` section.
|
|||||||
|
|
||||||
### Event Viewer options
|
### Event Viewer options
|
||||||
|
|
||||||
All variables listed are under a `event_viewer:` section.
|
The `event_viewer` is used for viewing all `clip` and `snapshot` media, in a media carousel. All variables listed are under a `event_viewer:` section.
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
@@ -133,6 +135,15 @@ All variables listed are under a `event_viewer:` section.
|
|||||||
| `draggable` | `true` | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
| `draggable` | `true` | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
||||||
| `controls.next_previous.style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
| `controls.next_previous.style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
||||||
| `controls.next_previous.size` | `48px` | The size of the next/previous controls (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].|
|
| `controls.next_previous.size` | `48px` | The size of the next/previous controls (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].|
|
||||||
|
| `actions` | | Actions to use for all views that use the `event_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.|
|
||||||
|
|
||||||
|
### Event Gallery options
|
||||||
|
|
||||||
|
The `event_gallery` is used for providing an overview of all `clips` and `snapshots` in a thumbnail gallery. All variables listed are under a `event_gallery:` section.
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| - | - | - |
|
||||||
|
| `actions` | | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.|
|
||||||
|
|
||||||
### Image options
|
### Image options
|
||||||
|
|
||||||
@@ -141,6 +152,7 @@ All variables listed are under a `image:` section.
|
|||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `src` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).|
|
| `src` | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views).|
|
||||||
|
| `actions` | | Actions to use for the `image` view. See [actions](#actions) below.|
|
||||||
|
|
||||||
### Dimension options
|
### Dimension options
|
||||||
|
|
||||||
@@ -457,6 +469,8 @@ elements:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<a name="frigate-card-action"></a>
|
||||||
|
|
||||||
#### Triggering card actions
|
#### Triggering card actions
|
||||||
|
|
||||||
You can control the card itself with the `custom:frigate-card-action` action.
|
You can control the card itself with the `custom:frigate-card-action` action.
|
||||||
@@ -516,10 +530,49 @@ See the [other options](#other-options) above.
|
|||||||
Clicking on a snapshot will take the user to a clip that was taken at the ~same
|
Clicking on a snapshot will take the user to a clip that was taken at the ~same
|
||||||
time as the snapshot (if any).
|
time as the snapshot (if any).
|
||||||
|
|
||||||
### Getting event details
|
<a name="actions"></a>
|
||||||
|
|
||||||
More details about an event can be found by clicking the 'globe' icon in the
|
## Card & View Actions
|
||||||
menu, which takes the user to the Frigate page for that event.
|
|
||||||
|
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
|
||||||
|
specified both for the overall card and for individual groups of view.
|
||||||
|
|
||||||
|
| Configuration path | Views to which it refers |
|
||||||
|
| - | - |
|
||||||
|
| `view.actions` | All (may be overriden by the below) |
|
||||||
|
| `event_viewer.actions` | `clip`, `snapshot` |
|
||||||
|
| `event_gallery.actions` | `clips`, `snapshots` |
|
||||||
|
| `live.actions` | `live` |
|
||||||
|
| `image.actions` | `image` |
|
||||||
|
|
||||||
|
If an action is configured for both the whole card (`view.actions`) and a more
|
||||||
|
specific view (e.g. `live.actions`) then the actions are merged, with the more
|
||||||
|
specific overriding the less specific (see example below).
|
||||||
|
|
||||||
|
The format for actions is the standard Home Assistant [action
|
||||||
|
format](https://www.home-assistant.io/lovelace/actions/#tap-action) as well as
|
||||||
|
the custom [Frigate card action](#frigate-card-action) to trigger card changes.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
In this example, double clicking the card in any view will cause the card to go
|
||||||
|
into fullscreen mode, **except** when the view is `live` in which case the
|
||||||
|
office lights are toggled.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
view:
|
||||||
|
actions:
|
||||||
|
double_tap_action:
|
||||||
|
action: custom:frigate-card-action
|
||||||
|
frigate_card_action: fullscreen
|
||||||
|
live:
|
||||||
|
provider: frigate-jsmpeg
|
||||||
|
actions:
|
||||||
|
entity: light.office_main_lights
|
||||||
|
double_tap_action:
|
||||||
|
action: toggle
|
||||||
|
```
|
||||||
|
|
||||||
## Menu Modes
|
## Menu Modes
|
||||||
|
|
||||||
@@ -536,7 +589,7 @@ This card supports several menu configurations.
|
|||||||
|
|
||||||
<a name="yaml-examples"></a>
|
<a name="yaml-examples"></a>
|
||||||
|
|
||||||
### Example YAML Configuration
|
## Example YAML Configuration
|
||||||
|
|
||||||
A configuration that uses WebRTC for live:
|
A configuration that uses WebRTC for live:
|
||||||
|
|
||||||
@@ -558,7 +611,7 @@ A configuration that shows the latest clip on load, but does not automatically p
|
|||||||
view_default: clip
|
view_default: clip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Screenshot: Snapshot / Clip Gallery
|
## Screenshot: Snapshot / Clip Gallery
|
||||||
|
|
||||||
Full viewing of clips:
|
Full viewing of clips:
|
||||||
|
|
||||||
|
|||||||
+81
-13
@@ -12,19 +12,23 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
import {
|
import {
|
||||||
|
ActionConfig,
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
LovelaceCardEditor,
|
LovelaceCardEditor,
|
||||||
getLovelace,
|
getLovelace,
|
||||||
|
handleAction,
|
||||||
|
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';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CardAction,
|
ActionType,
|
||||||
entitySchema,
|
|
||||||
frigateCardConfigSchema,
|
|
||||||
GetFrigateCardMenuButtonParameters,
|
GetFrigateCardMenuButtonParameters,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
|
entitySchema,
|
||||||
|
frigateCardConfigSchema,
|
||||||
|
Actions,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import type {
|
import type {
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
@@ -38,10 +42,16 @@ import type {
|
|||||||
|
|
||||||
import { CARD_VERSION, REPO_URL } from './const.js';
|
import { CARD_VERSION, REPO_URL } from './const.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON, MENU_HEIGHT } from './components/menu.js';
|
import {
|
||||||
|
FRIGATE_BUTTON_MENU_ICON,
|
||||||
|
MENU_HEIGHT,
|
||||||
|
FrigateCardMenu,
|
||||||
|
} from './components/menu.js';
|
||||||
import { View } from './view.js';
|
import { View } from './view.js';
|
||||||
import {
|
import {
|
||||||
|
convertActionToFrigateCardCustomAction,
|
||||||
createFrigateCardCustomAction,
|
createFrigateCardCustomAction,
|
||||||
|
getActionConfigGivenAction,
|
||||||
homeAssistantSignPath,
|
homeAssistantSignPath,
|
||||||
homeAssistantWSRequest,
|
homeAssistantWSRequest,
|
||||||
isValidMediaShowInfo,
|
isValidMediaShowInfo,
|
||||||
@@ -65,14 +75,15 @@ import cardStyle from './scss/card.scss';
|
|||||||
import { ResolvedMediaCache } from './resolved-media.js';
|
import { ResolvedMediaCache } from './resolved-media.js';
|
||||||
import { BrowseMediaUtil } from './browse-media-util.js';
|
import { BrowseMediaUtil } from './browse-media-util.js';
|
||||||
import { isConfigUpgradeable } from './config-mgmt.js';
|
import { isConfigUpgradeable } from './config-mgmt.js';
|
||||||
|
import { actionHandler } from './action-handler-directive.js';
|
||||||
|
|
||||||
/** A note on media callbacks:
|
/** A note on media callbacks:
|
||||||
*
|
*
|
||||||
* We need media elements (e.g. <video>, <img> or <canvas>) to callback when:
|
* Media elements (e.g. <video>, <img> or <canvas>) need to callback when:
|
||||||
* - Metadata is loaded / dimensions are known (for aspect-ratio)
|
* - Metadata is loaded / dimensions are known (for aspect-ratio)
|
||||||
* - Media is playing / paused (to avoid reloading)
|
* - Media is playing / paused (to avoid reloading)
|
||||||
*
|
*
|
||||||
* There are a number of different approaches used to attach event handlers to
|
* A number of different approaches used to attach event handlers to
|
||||||
* get these callbacks (which need to be attached directly to the media
|
* get these callbacks (which need to be attached directly to the media
|
||||||
* elements, which may be 'buried' down the DOM):
|
* elements, which may be 'buried' down the DOM):
|
||||||
* - Extend the `ha-hls-player` and `ha-camera-stream` to specify the required
|
* - Extend the `ha-hls-player` and `ha-camera-stream` to specify the required
|
||||||
@@ -83,6 +94,15 @@ import { isConfigUpgradeable } from './config-mgmt.js';
|
|||||||
* - Directly specifying hooks (e.g. for snapshot viewing with simple <img> tags)
|
* - Directly specifying hooks (e.g. for snapshot viewing with simple <img> tags)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** A note on action/menu/ll-custom events:
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* handled by handleAction() from custom-card-helpers. For Frigate-card specific
|
||||||
|
* actions, handleAction() call will result in an ll-custom DOM event being
|
||||||
|
* fired, which needs to be caught at the card level to handle.
|
||||||
|
*/
|
||||||
|
|
||||||
/* eslint no-console: 0 */
|
/* eslint no-console: 0 */
|
||||||
console.info(
|
console.info(
|
||||||
`%c FRIGATE-HASS-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
|
`%c FRIGATE-HASS-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `,
|
||||||
@@ -106,7 +126,7 @@ console.info(
|
|||||||
@customElement('frigate-card')
|
@customElement('frigate-card')
|
||||||
export class FrigateCard extends LitElement {
|
export class FrigateCard extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected _hass: (HomeAssistant & ExtendedHomeAssistant) | null = null;
|
protected _hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
public config!: FrigateCardConfig;
|
public config!: FrigateCardConfig;
|
||||||
@@ -580,11 +600,12 @@ export class FrigateCard extends LitElement {
|
|||||||
* Handle a request for a card action.
|
* Handle a request for a card action.
|
||||||
* @param action The action requested (e.g. clips, fullscreen)
|
* @param action The action requested (e.g. clips, fullscreen)
|
||||||
*/
|
*/
|
||||||
protected _cardActionHandler(event: CustomEvent<CardAction>): void {
|
protected _cardActionHandler(event: CustomEvent<ActionType>): void {
|
||||||
const action = event.detail.action;
|
const frigateCardAction = convertActionToFrigateCardCustomAction(event.detail);
|
||||||
if (!action) {
|
if (!frigateCardAction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const action = frigateCardAction.frigate_card_action;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'frigate':
|
case 'frigate':
|
||||||
@@ -649,6 +670,29 @@ export class FrigateCard extends LitElement {
|
|||||||
}, this.config.view.timeout * 1000);
|
}, this.config.view.timeout * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _actionHandler(
|
||||||
|
ev: CustomEvent,
|
||||||
|
config?: {
|
||||||
|
hold_action?: ActionType;
|
||||||
|
tap_action?: ActionType;
|
||||||
|
double_tap_action?: ActionType;
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
const interaction = ev.detail.action;
|
||||||
|
const node: HTMLElement | null = ev.currentTarget as HTMLElement | null;
|
||||||
|
if (
|
||||||
|
config &&
|
||||||
|
node &&
|
||||||
|
interaction &&
|
||||||
|
// Don't call handleAction() unless there is explicitly an action defined
|
||||||
|
// (as it uses a default that is unhelpful for views that have default
|
||||||
|
// tap/click actions).
|
||||||
|
getActionConfigGivenAction(interaction, config)
|
||||||
|
) {
|
||||||
|
handleAction(node, this._hass as HomeAssistant, config, ev.detail.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the card menu.
|
* Render the card menu.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -663,7 +707,6 @@ export class FrigateCard extends LitElement {
|
|||||||
.menuConfig=${this.config.menu}
|
.menuConfig=${this.config.menu}
|
||||||
.buttons=${this._getMenuButtons()}
|
.buttons=${this._getMenuButtons()}
|
||||||
class="${classMap(classes)}"
|
class="${classMap(classes)}"
|
||||||
@frigate-card:card-action=${this._cardActionHandler.bind(this)}
|
|
||||||
></frigate-card-menu>
|
></frigate-card-menu>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -823,6 +866,21 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _getMergedActions(): Actions {
|
||||||
|
let specificActions: Actions | undefined = undefined;
|
||||||
|
|
||||||
|
if (this._view.is('live')) {
|
||||||
|
specificActions = this.config.live.actions;
|
||||||
|
} else if (this._view.isGalleryView()) {
|
||||||
|
specificActions = this.config.event_gallery?.actions;
|
||||||
|
} else if (this._view.isViewerView()) {
|
||||||
|
specificActions = this.config.event_viewer.actions;
|
||||||
|
} else if (this._view.is('image')) {
|
||||||
|
specificActions = this.config.image?.actions;
|
||||||
|
}
|
||||||
|
return { ...this.config.view.actions, ...specificActions };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method for the card.
|
* Master render method for the card.
|
||||||
*/
|
*/
|
||||||
@@ -868,7 +926,18 @@ export class FrigateCard extends LitElement {
|
|||||||
absolute: padding != null,
|
absolute: padding != null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return html` <ha-card @click=${this._interactionHandler}>
|
const actions = this._getMergedActions();
|
||||||
|
|
||||||
|
return html` <ha-card
|
||||||
|
@click=${this._interactionHandler}
|
||||||
|
.actionHandler=${actionHandler({
|
||||||
|
hasHold: hasAction(actions.hold_action),
|
||||||
|
hasDoubleClick: hasAction(actions.double_tap_action),
|
||||||
|
})}
|
||||||
|
@action=${(ev: CustomEvent) =>
|
||||||
|
this._actionHandler(ev, actions)}
|
||||||
|
@ll-custom=${this._cardActionHandler.bind(this)}
|
||||||
|
>
|
||||||
${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
|
${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
|
||||||
<div class="container outer" style="${styleMap(outerStyle)}">
|
<div class="container outer" style="${styleMap(outerStyle)}">
|
||||||
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
|
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
|
||||||
@@ -999,7 +1068,6 @@ export class FrigateCard extends LitElement {
|
|||||||
// 'frigate-card-elements' to re-render (by being a property).
|
// 'frigate-card-elements' to re-render (by being a property).
|
||||||
e.view = this._view;
|
e.view = this._view;
|
||||||
}}
|
}}
|
||||||
@frigate-card:card-action=${this._cardActionHandler.bind(this)}
|
|
||||||
>
|
>
|
||||||
</frigate-card-elements>
|
</frigate-card-elements>
|
||||||
`
|
`
|
||||||
|
|||||||
+23
-13
@@ -3,7 +3,7 @@ import { MessageBase } from 'home-assistant-js-websocket';
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
import {
|
import {
|
||||||
ElementsActionType,
|
ActionType,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
frigateCardCustomActionSchema,
|
frigateCardCustomActionSchema,
|
||||||
@@ -258,7 +258,9 @@ export function isValidMediaShowInfo(info: MediaShowInfo): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertActionToFrigateCardCustomAction(action: ElementsActionType): FrigateCardCustomAction | null {
|
export function convertActionToFrigateCardCustomAction(
|
||||||
|
action: ActionType,
|
||||||
|
): FrigateCardCustomAction | null {
|
||||||
// Parse a custom event as other things could generate ll-custom events that
|
// Parse a custom event as other things could generate ll-custom events that
|
||||||
// are not related to Frigate Card.
|
// are not related to Frigate Card.
|
||||||
const parseResult = frigateCardCustomActionSchema.safeParse(action);
|
const parseResult = frigateCardCustomActionSchema.safeParse(action);
|
||||||
@@ -269,18 +271,26 @@ export function createFrigateCardCustomAction(action: string): FrigateCardCustom
|
|||||||
return {
|
return {
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
frigate_card_action: action,
|
frigate_card_action: action,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertLovelaceEventToCardActionEvent(
|
export function getActionConfigGivenAction(
|
||||||
node: HTMLElement,
|
interaction?: string,
|
||||||
ev: CustomEvent,
|
config?: {
|
||||||
): void {
|
hold_action?: ActionType;
|
||||||
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
|
tap_action?: ActionType;
|
||||||
if (frigateCardAction) {
|
double_tap_action?: ActionType;
|
||||||
ev.stopPropagation();
|
},
|
||||||
dispatchFrigateCardEvent(node, 'card-action', {
|
): ActionType | null {
|
||||||
action: frigateCardAction.frigate_card_action,
|
if (!interaction || !config) {
|
||||||
});
|
return null;
|
||||||
}
|
}
|
||||||
|
if (interaction == 'tap' && config.tap_action) {
|
||||||
|
return config.tap_action;
|
||||||
|
} else if (interaction == 'hold' && config.hold_action) {
|
||||||
|
return config.hold_action;
|
||||||
|
} else if (interaction == 'double_tap' && config.double_tap_action) {
|
||||||
|
return config.double_tap_action;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
PictureElements,
|
PictureElements,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import {
|
import {
|
||||||
convertLovelaceEventToCardActionEvent,
|
|
||||||
dispatchErrorMessageEvent,
|
dispatchErrorMessageEvent,
|
||||||
dispatchFrigateCardEvent,
|
dispatchFrigateCardEvent,
|
||||||
} from '../common.js';
|
} from '../common.js';
|
||||||
@@ -184,7 +183,6 @@ export class FrigateCardElements extends LitElement {
|
|||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.elements=${this.elements}
|
.elements=${this.elements}
|
||||||
@ll-custom=${(ev: CustomEvent) => convertLovelaceEventToCardActionEvent(this, ev)}
|
|
||||||
>
|
>
|
||||||
</frigate-card-elements-core>`;
|
</frigate-card-elements-core>`;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-31
@@ -15,16 +15,13 @@ import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
|||||||
import { actionHandler } from '../action-handler-directive.js';
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CardAction,
|
|
||||||
ElementsActionType,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuConfig,
|
MenuConfig,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
convertLovelaceEventToCardActionEvent,
|
getActionConfigGivenAction,
|
||||||
dispatchFrigateCardEvent,
|
|
||||||
shouldUpdateBasedOnHass,
|
shouldUpdateBasedOnHass,
|
||||||
} from '../common.js';
|
} from '../common.js';
|
||||||
|
|
||||||
@@ -54,21 +51,13 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public buttons: MenuButton[] = [];
|
public buttons: MenuButton[] = [];
|
||||||
|
|
||||||
protected _interactionHandler(ev: CustomEvent, button: MenuButton): void {
|
protected _actionHandler(ev: CustomEvent, button: MenuButton): void {
|
||||||
if (!ev) {
|
if (!ev) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const interaction: string = ev.detail.action;
|
const interaction: string = ev.detail.action;
|
||||||
let action: ElementsActionType | undefined;
|
const action = getActionConfigGivenAction(interaction, button);
|
||||||
|
|
||||||
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) {
|
if (!action) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,29 +65,20 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
// 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.
|
||||||
const frigateCardAction = convertActionToFrigateCardCustomAction(action);
|
const frigateCardAction = convertActionToFrigateCardCustomAction(action);
|
||||||
if (frigateCardAction) {
|
if (
|
||||||
if (frigateCardAction.frigate_card_action == 'frigate') {
|
frigateCardAction &&
|
||||||
|
frigateCardAction.frigate_card_action == 'frigate' &&
|
||||||
|
this._menuConfig?.mode.startsWith('hidden-')
|
||||||
|
) {
|
||||||
// If the user presses the frigate button and it's a hide-away menu,
|
// If the user presses the frigate button and it's a hide-away menu,
|
||||||
// then expand the menu and return.
|
// then expand the menu and return.
|
||||||
if (this._menuConfig?.mode.startsWith('hidden-')) {
|
|
||||||
this.expand = !this.expand;
|
this.expand = !this.expand;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Collapse menu after the user clicks on something.
|
// Collapse menu after the user clicks on something.
|
||||||
this.expand = false;
|
this.expand = false;
|
||||||
|
handleAction(this, this.hass as HomeAssistant, button, interaction);
|
||||||
dispatchFrigateCardEvent<CardAction>(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.
|
// Determine whether the menu should be updated.
|
||||||
@@ -173,8 +153,7 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
icon=${icon || 'mdi:gesture-tap-button'}
|
icon=${icon || 'mdi:gesture-tap-button'}
|
||||||
.label=${title || ''}
|
.label=${title || ''}
|
||||||
title=${title || ''}
|
title=${title || ''}
|
||||||
@action=${(ev) => this._interactionHandler(ev, button)}
|
@action=${(ev) => this._actionHandler(ev, button)}
|
||||||
@ll-custom=${(ev: CustomEvent) => convertLovelaceEventToCardActionEvent(this, ev)}
|
|
||||||
.actionHandler=${actionHandler({
|
.actionHandler=${actionHandler({
|
||||||
hasHold: hasHold,
|
hasHold: hasHold,
|
||||||
hasDoubleClick: hasDoubleClick,
|
hasDoubleClick: hasDoubleClick,
|
||||||
|
|||||||
+31
-11
@@ -117,7 +117,7 @@ export const frigateCardCustomActionSchema = customActionSchema.merge(
|
|||||||
);
|
);
|
||||||
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
||||||
|
|
||||||
const elementsActionSchema = z.union([
|
const actionSchema = z.union([
|
||||||
toggleActionSchema,
|
toggleActionSchema,
|
||||||
callServiceActionSchema,
|
callServiceActionSchema,
|
||||||
navigateActionSchema,
|
navigateActionSchema,
|
||||||
@@ -125,15 +125,28 @@ const elementsActionSchema = z.union([
|
|||||||
moreInfoActionSchema,
|
moreInfoActionSchema,
|
||||||
frigateCardCustomActionSchema,
|
frigateCardCustomActionSchema,
|
||||||
]);
|
]);
|
||||||
export type ElementsActionType = z.infer<typeof elementsActionSchema>;
|
export type ActionType = z.infer<typeof actionSchema>;
|
||||||
|
|
||||||
const elementsBaseSchema = z.object({
|
const actionBaseSchema = z.object({
|
||||||
|
tap_action: actionSchema.optional(),
|
||||||
|
hold_action: actionSchema.optional(),
|
||||||
|
double_tap_action: actionSchema.optional(),
|
||||||
|
}).passthrough();
|
||||||
|
export type Actions = z.infer<typeof actionBaseSchema>;
|
||||||
|
|
||||||
|
const actionsSchema = z.object({
|
||||||
|
// Passthrough to allow (at least) entity/camera_image to go through. This
|
||||||
|
// card doesn't need these attributes, but handleAction() in
|
||||||
|
// custom_card_helpers may depending on how the action is configured.
|
||||||
|
actions: actionBaseSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementsBaseSchema = actionBaseSchema.merge(
|
||||||
|
z.object({
|
||||||
style: z.object({}).passthrough().optional(),
|
style: z.object({}).passthrough().optional(),
|
||||||
title: z.string().nullable().optional(),
|
title: z.string().nullable().optional(),
|
||||||
tap_action: elementsActionSchema.optional(),
|
}),
|
||||||
hold_action: elementsActionSchema.optional(),
|
);
|
||||||
double_tap_action: elementsActionSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Picture Element Configuration.
|
* Picture Element Configuration.
|
||||||
@@ -322,6 +335,7 @@ const viewConfigSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.default(viewConfigDefault.timeout),
|
.default(viewConfigDefault.timeout),
|
||||||
})
|
})
|
||||||
|
.merge(actionsSchema)
|
||||||
.default(viewConfigDefault);
|
.default(viewConfigDefault);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -331,6 +345,7 @@ const imageConfigSchema = z
|
|||||||
.object({
|
.object({
|
||||||
src: z.string().optional(),
|
src: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.merge(actionsSchema)
|
||||||
.optional();
|
.optional();
|
||||||
export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
|
export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
|
||||||
|
|
||||||
@@ -356,6 +371,7 @@ const liveConfigSchema = z
|
|||||||
preload: z.boolean().default(liveConfigDefault.preload),
|
preload: z.boolean().default(liveConfigDefault.preload),
|
||||||
webrtc: webrtcConfigSchema,
|
webrtc: webrtcConfigSchema,
|
||||||
})
|
})
|
||||||
|
.merge(actionsSchema)
|
||||||
.default(liveConfigDefault);
|
.default(liveConfigDefault);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -430,9 +446,16 @@ const viewerConfigSchema = z
|
|||||||
})
|
})
|
||||||
.default(viewerConfigDefault.controls),
|
.default(viewerConfigDefault.controls),
|
||||||
})
|
})
|
||||||
|
.merge(actionsSchema)
|
||||||
.default(viewerConfigDefault);
|
.default(viewerConfigDefault);
|
||||||
export type ViewerConfig = z.infer<typeof viewerConfigSchema>;
|
export type ViewerConfig = z.infer<typeof viewerConfigSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event gallery configuration section (clips, snapshots).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const galleryConfigSchema = actionsSchema.optional();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dimensions configuration section.
|
* Dimensions configuration section.
|
||||||
*/
|
*/
|
||||||
@@ -471,6 +494,7 @@ export const frigateCardConfigSchema = z.object({
|
|||||||
menu: menuConfigSchema,
|
menu: menuConfigSchema,
|
||||||
live: liveConfigSchema,
|
live: liveConfigSchema,
|
||||||
event_viewer: viewerConfigSchema,
|
event_viewer: viewerConfigSchema,
|
||||||
|
event_gallery: galleryConfigSchema,
|
||||||
image: imageConfigSchema,
|
image: imageConfigSchema,
|
||||||
elements: pictureElementsSchema,
|
elements: pictureElementsSchema,
|
||||||
dimensions: dimensionsConfigSchema,
|
dimensions: dimensionsConfigSchema,
|
||||||
@@ -537,10 +561,6 @@ export interface Message {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardAction {
|
|
||||||
action: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Home Assistant API types.
|
* Home Assistant API types.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user