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