218 lines
6.5 KiB
TypeScript
218 lines
6.5 KiB
TypeScript
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
|
import { LitElement } from 'lit';
|
|
import { FRIGATE_ICON_SVG_PATH } from '../camera-manager/frigate/icon.js';
|
|
import type {
|
|
ActionType,
|
|
ActionsConfig,
|
|
MenuConfig,
|
|
MenuItem,
|
|
} from '../config/types.js';
|
|
import { FRIGATE_BUTTON_MENU_ICON } from '../const.js';
|
|
import { StateParameters } from '../types.js';
|
|
import {
|
|
convertActionToCardCustomAction,
|
|
getActionConfigGivenAction,
|
|
} from '../utils/action';
|
|
import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js';
|
|
import { refreshDynamicStateParameters } from '../utils/ha/index.js';
|
|
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request.js';
|
|
|
|
export class MenuController {
|
|
protected _host: LitElement;
|
|
protected _config: MenuConfig | null = null;
|
|
protected _buttons: MenuItem[] = [];
|
|
protected _expanded = false;
|
|
|
|
constructor(host: LitElement) {
|
|
this._host = host;
|
|
}
|
|
|
|
public setMenuConfig(config: MenuConfig): void {
|
|
this._config = config;
|
|
this._host.style.setProperty(
|
|
'--frigate-card-menu-button-size',
|
|
`${config.button_size}px`,
|
|
);
|
|
|
|
// Store the menu style, position and alignment as attributes (used for
|
|
// styling).
|
|
this._host.setAttribute('data-style', config.style);
|
|
this._host.setAttribute('data-position', config.position);
|
|
this._host.setAttribute('data-alignment', config.alignment);
|
|
|
|
this._sortButtons();
|
|
this._host.requestUpdate();
|
|
}
|
|
|
|
public getMenuConfig(): MenuConfig | null {
|
|
return this._config;
|
|
}
|
|
|
|
public isExpanded(): boolean {
|
|
return this._expanded;
|
|
}
|
|
|
|
public setButtons(buttons: MenuItem[]): void {
|
|
this._buttons = buttons;
|
|
this._sortButtons();
|
|
this._host.requestUpdate();
|
|
}
|
|
|
|
public getButtons(alignment: 'matching' | 'opposing'): MenuItem[] {
|
|
const style = this._config?.style;
|
|
|
|
const aligned = (button: MenuItem): boolean => {
|
|
return (
|
|
button.alignment === alignment || (alignment === 'matching' && !button.alignment)
|
|
);
|
|
};
|
|
|
|
const enabled = (button: MenuItem): boolean => {
|
|
return button.enabled !== false;
|
|
};
|
|
|
|
const suitableToShowIfHiddenMenu = (button: MenuItem): boolean => {
|
|
// If the hidden menu isn't expanded, only show the Frigate button.
|
|
return (
|
|
style !== 'hidden' || this._expanded || button.icon === FRIGATE_BUTTON_MENU_ICON
|
|
);
|
|
};
|
|
|
|
return this._buttons.filter(
|
|
(button) =>
|
|
enabled(button) && aligned(button) && suitableToShowIfHiddenMenu(button),
|
|
);
|
|
}
|
|
|
|
public setExpanded(expanded: boolean): void {
|
|
this._expanded = expanded;
|
|
setOrRemoveAttribute(this._host, expanded, 'expanded');
|
|
this._host.requestUpdate();
|
|
}
|
|
|
|
public toggleExpanded(): void {
|
|
this.setExpanded(!this._expanded);
|
|
}
|
|
|
|
public actionHandler(
|
|
ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
|
|
config?: ActionsConfig,
|
|
): void {
|
|
// These interactions should only be handled by the menu, as nothing
|
|
// upstream has the user-provided configuration.
|
|
ev.stopPropagation();
|
|
|
|
// If the event itself contains a configuration then use that. This is
|
|
// useful in cases where the registration of the event handler does not have
|
|
// access to the actual desired configuration (e.g. action events generated
|
|
// by a submenu).
|
|
if (ev.detail.config) {
|
|
config = ev.detail.config;
|
|
}
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
const interaction: string = ev.detail.action;
|
|
const action = getActionConfigGivenAction(interaction, config);
|
|
if (!action) {
|
|
return;
|
|
}
|
|
const actions = arrayify(action);
|
|
|
|
// A note on the complexity below: By default the menu should close when a
|
|
// user takes an action, an exception is if the user is specifically
|
|
// manipulating the menu in the actions themselves.
|
|
let menuToggle = false;
|
|
|
|
const toggleLessActions = actions.filter(
|
|
(item) => isTruthy(item) && !this._isMenuToggleAction(item),
|
|
);
|
|
if (toggleLessActions.length != actions.length) {
|
|
menuToggle = true;
|
|
}
|
|
|
|
if (toggleLessActions.length) {
|
|
dispatchActionExecutionRequest(this._host, {
|
|
action: actions,
|
|
config: config,
|
|
});
|
|
}
|
|
|
|
if (this._isHidingMenu()) {
|
|
if (menuToggle) {
|
|
this.setExpanded(!this._expanded);
|
|
} else {
|
|
// Don't close the menu if there is another action to come.
|
|
const holdAction = getActionConfigGivenAction('hold', config);
|
|
const doubleTapAction = getActionConfigGivenAction('double_tap', config);
|
|
const tapAction = getActionConfigGivenAction('tap', config);
|
|
const endTapAction = getActionConfigGivenAction('end_tap', config);
|
|
|
|
if (
|
|
interaction === 'end_tap' ||
|
|
(interaction === 'start_tap' &&
|
|
!holdAction &&
|
|
!doubleTapAction &&
|
|
!tapAction &&
|
|
!endTapAction) ||
|
|
(interaction !== 'end_tap' && !endTapAction)
|
|
) {
|
|
this.setExpanded(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public getFreshButtonState(hass: HomeAssistant, button: MenuItem): StateParameters {
|
|
const stateParameters = { ...button } as StateParameters;
|
|
return hass && button.type === 'custom:frigate-card-menu-state-icon'
|
|
? refreshDynamicStateParameters(hass, stateParameters)
|
|
: stateParameters;
|
|
}
|
|
|
|
public getSVGPath(button: MenuItem): string {
|
|
return button.icon === FRIGATE_BUTTON_MENU_ICON ? FRIGATE_ICON_SVG_PATH : '';
|
|
}
|
|
|
|
protected _sortButtons(): void {
|
|
const style = this._config?.style;
|
|
const sortButtons = (a: MenuItem, b: MenuItem): number => {
|
|
// If the menu is hidden, the Frigate button must come first.
|
|
if (style === 'hidden') {
|
|
if (a.icon === FRIGATE_BUTTON_MENU_ICON) {
|
|
return -1;
|
|
} else if (b.icon === FRIGATE_BUTTON_MENU_ICON) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Otherwise sort by priority.
|
|
if (
|
|
a.priority === undefined ||
|
|
(b.priority !== undefined && b.priority > a.priority)
|
|
) {
|
|
return 1;
|
|
}
|
|
if (
|
|
b.priority === undefined ||
|
|
(a.priority !== undefined && b.priority < a.priority)
|
|
) {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
this._buttons.sort(sortButtons);
|
|
}
|
|
|
|
protected _isHidingMenu(): boolean {
|
|
return this._config?.style === 'hidden' ?? false;
|
|
}
|
|
|
|
protected _isMenuToggleAction(action: ActionType): boolean {
|
|
const frigateCardAction = convertActionToCardCustomAction(action);
|
|
return !!frigateCardAction && frigateCardAction.frigate_card_action == 'menu_toggle';
|
|
}
|
|
}
|