Files
advanced-camera-card/src/components/menu.ts
T

194 lines
6.1 KiB
TypeScript

import { HassEntity } from 'home-assistant-js-websocket';
import { HomeAssistant, hasAction, stateIcon } from 'custom-card-helpers';
import {
CSSResultGroup,
LitElement,
TemplateResult,
html,
unsafeCSS,
PropertyValues,
} 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 { actionHandler } from '../action-handler-directive.js';
import type {
ExtendedHomeAssistant,
FrigateMenuMode,
MenuButton,
MenuInteraction,
} from '../types.js';
import { dispatchFrigateCardEvent, shouldUpdateBasedOnHass } from '../common.js';
import menuStyle from '../scss/menu.scss';
export const MENU_HEIGHT = 46;
// A menu for the Frigate card.
@customElement('frigate-card-menu')
export class FrigateCardMenu extends LitElement {
@property({ attribute: false })
public hass!: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false })
protected menuMode: FrigateMenuMode = 'hidden-top';
@property({ attribute: false })
protected expand = false;
@property({ attribute: false })
public buttons: MenuButton[] = [];
protected _interactionHandler(ev: CustomEvent, button: MenuButton): void {
if (this.menuMode.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;
}
dispatchFrigateCardEvent<MenuInteraction>(this, 'menu-interaction', {
interaction: ev.detail.action,
button: button,
});
}
// Determine whether the menu should be updated.
protected shouldUpdate(changedProps: PropertyValues): boolean {
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
if (changedProps.size > 1 || !oldHass) {
return true;
}
// Extract the entities the menu rendering depends on (if any).
const entities: string[] = [];
for (let i = 0; i < this.buttons.length; i++) {
const button = this.buttons[i];
if (button.type == 'custom:frigate-card-menu-state-icon') {
entities.push(button.entity);
}
}
return shouldUpdateBasedOnHass(this.hass, oldHass, entities);
}
// Render a menu button.
protected _renderButton(button: MenuButton): TemplateResult {
let state: HassEntity | null = null;
let emphasize = false;
let title = button.title;
let icon = button.icon;
const style = ('style' in button ? button.style : {}) || {};
if (button.type === 'custom:frigate-card-menu-state-icon') {
state = this.hass.states[button.entity];
emphasize =
!!state && button.state_color && ['on', 'active', 'home'].includes(state.state);
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 classes = {
button: true,
emphasize: emphasize,
};
// TODO: Upon a safe distance from the release of HA 2021.11 these
// attributes can be removed from the <ha-icon-button>.
// - icon (replaced with the embedded <ha-icon>)
// - title (replaced with .label)
return html` <ha-icon-button
class="${classMap(classes)}"
style="${styleMap(style)}"
icon=${icon || 'mdi:gesture-tap-button'}
.label=${title || ''}
title=${title || ''}
@action=${(ev) => this._interactionHandler(ev, button)}
.actionHandler=${actionHandler({
hasHold: hasHold,
hasDoubleClick: hasDoubleClick,
})}
>
<ha-icon icon="${icon || 'mdi:gesture-tap-button'}"></ha-icon>
</ha-icon-button>`;
}
// Render the Frigate menu button.
protected _renderFrigateButton(button: MenuButton): TemplateResult {
const icon =
this.menuMode.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 {
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 (
this.menuMode == 'none' ||
(this.menuMode.startsWith('hidden-') && !this.buttons.find(isFrigateButton))
) {
return html``;
}
const classes = {
'frigate-card-menu': true,
'overlay-hidden':
this.menuMode.startsWith('hidden-') ||
this.menuMode.startsWith('overlay-') ||
this.menuMode.startsWith('hover-'),
'expanded-horizontal':
(this.menuMode.startsWith('overlay-') ||
this.menuMode.startsWith('hover-') ||
this.expand) &&
(this.menuMode.endsWith('-top') || this.menuMode.endsWith('-bottom')),
'expanded-vertical':
(this.menuMode.startsWith('overlay-') ||
this.menuMode.startsWith('hover-') ||
this.expand) &&
(this.menuMode.endsWith('-left') || this.menuMode.endsWith('-right')),
full: ['above', 'below'].includes(this.menuMode),
left: this.menuMode.endsWith('-left'),
right: this.menuMode.endsWith('-right'),
top: this.menuMode.endsWith('-top'),
bottom: this.menuMode.endsWith('-bottom'),
};
return html`
<div class=${classMap(classes)}>
${Array.from(this.buttons).map((button) => {
return isFrigateButton(button)
? this._renderFrigateButton(button)
: this._renderButton(button);
})}
</div>
`;
}
// Return compiled CSS styles (thus safe to use with unsafeCSS).
static get styles(): CSSResultGroup {
return unsafeCSS(menuStyle);
}
}