Rework menu to use elements.
This commit is contained in:
+10
-10
@@ -16,28 +16,28 @@
|
||||
"dependencies": {
|
||||
"@cycjimmy/jsmpeg-player": "^5.0.1",
|
||||
"@material/image-list": "^12.0.0",
|
||||
"custom-card-helpers": "^1.7.2",
|
||||
"dayjs": "^1.10.6",
|
||||
"custom-card-helpers": "^1.8.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"lit": "^2.0.0-rc.2",
|
||||
"lit": "^2.0.0",
|
||||
"screenfull": "^5.1.0",
|
||||
"zod": "^3.8.2"
|
||||
"zod": "^3.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.15.4",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
||||
"@typescript-eslint/parser": "^4.30.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.32.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"npm-check-updates": "^11.8.5",
|
||||
"prettier": "^2.3.2",
|
||||
"rollup": "^2.56.3",
|
||||
"prettier": "^2.4.1",
|
||||
"rollup": "^2.58.0",
|
||||
"rollup-plugin-babel": "^4.4.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
@@ -45,8 +45,8 @@
|
||||
"rollup-plugin-styles": "^3.14.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.30.0",
|
||||
"sass": "^1.39.0",
|
||||
"typescript": "^4.4.2"
|
||||
"sass": "^1.42.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "rollup -c rollup.config.dev.js --watch",
|
||||
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
} from 'custom-card-helpers/dist/types';
|
||||
import { fireEvent } from 'custom-card-helpers';
|
||||
|
||||
const isTouch =
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (navigator as any).msMaxTouchPoints > 0;
|
||||
|
||||
interface ActionHandler extends HTMLElement {
|
||||
holdTime: number;
|
||||
|
||||
+61
-73
@@ -14,9 +14,9 @@ import { until } from 'lit/directives/until';
|
||||
import {
|
||||
HomeAssistant,
|
||||
LovelaceCardEditor,
|
||||
fireEvent,
|
||||
getLovelace,
|
||||
stateIcon,
|
||||
handleAction,
|
||||
ActionHandlerEvent,
|
||||
} from 'custom-card-helpers';
|
||||
import screenfull from 'screenfull';
|
||||
|
||||
@@ -33,7 +33,12 @@ import type {
|
||||
import { CARD_VERSION } from './const';
|
||||
import { FrigateCardMenu, MENU_HEIGHT } from './components/menu';
|
||||
import { View } from './view';
|
||||
import { getParseErrorKeys, homeAssistantWSRequest } from './common';
|
||||
import { actionHandler } from './action-handler-directive';
|
||||
import {
|
||||
getParseErrorKeys,
|
||||
homeAssistantWSRequest,
|
||||
shouldUpdateBasedOnHass,
|
||||
} from './common';
|
||||
import { localize } from './localize/localize';
|
||||
import { renderErrorMessage, renderProgressIndicator } from './components/message';
|
||||
|
||||
@@ -83,34 +88,6 @@ console.info(
|
||||
description: localize('common.frigate_card_description'),
|
||||
});
|
||||
|
||||
// Determine whether the card should be updated based on Home Assistant changes.
|
||||
function shouldUpdateBasedOnHass(
|
||||
newHass: HomeAssistant | null,
|
||||
oldHass: HomeAssistant | undefined,
|
||||
entities: string[] | null,
|
||||
): boolean {
|
||||
if (!newHass || !entities) {
|
||||
return false;
|
||||
}
|
||||
if (!entities.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oldHass) {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const entity = entities[i];
|
||||
if (!entity) {
|
||||
continue;
|
||||
}
|
||||
if (oldHass.states[entity] !== newHass.states[entity]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Main FrigateCard class.
|
||||
@customElement('frigate-card')
|
||||
export class FrigateCard extends LitElement {
|
||||
@@ -144,7 +121,13 @@ export class FrigateCard extends LitElement {
|
||||
|
||||
set hass(hass: HomeAssistant & ExtendedHomeAssistant) {
|
||||
this._hass = hass;
|
||||
this._updateMenu();
|
||||
|
||||
// Manually set hass in the menu. This is to allow the menu to update,
|
||||
// without necessarily re-rendering the entire card (re-rendering interrupts
|
||||
// clip playing).
|
||||
if (this._menu && this._hass) {
|
||||
this._menu.hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the configuration element.
|
||||
@@ -157,68 +140,66 @@ export class FrigateCard extends LitElement {
|
||||
return {};
|
||||
}
|
||||
|
||||
protected _updateMenu(): void {
|
||||
// Manually set hass in the menu. This is to allow the menu to update,
|
||||
// without necessarily re-rendering the entire card (re-rendering interrupts
|
||||
// clip playing).
|
||||
if (!this._menu || !this._hass) {
|
||||
return;
|
||||
}
|
||||
this._menu.buttons = this._getMenuButtons();
|
||||
}
|
||||
|
||||
protected _getMenuButtons(): Map<string, MenuButton> {
|
||||
const buttons: Map<string, MenuButton> = new Map();
|
||||
protected _getMenuButtons(): MenuButton[] {
|
||||
const buttons: MenuButton[] = [];
|
||||
|
||||
if (this.config.menu_buttons?.frigate ?? true) {
|
||||
buttons.set('frigate', { description: localize('menu.frigate') });
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
card_action: 'frigate',
|
||||
title: localize('menu.frigate'),
|
||||
});
|
||||
}
|
||||
if (this.config.menu_buttons?.live ?? true) {
|
||||
buttons.set('live', {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
card_action: 'live',
|
||||
title: localize('menu.live'),
|
||||
icon: 'mdi:cctv',
|
||||
description: localize('menu.live'),
|
||||
emphasize: this._view.is('live'),
|
||||
});
|
||||
}
|
||||
if (this.config.menu_buttons?.clips ?? true) {
|
||||
buttons.set('clips', {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
card_action: 'clips',
|
||||
title: localize('menu.clips'),
|
||||
icon: 'mdi:filmstrip',
|
||||
description: localize('menu.clips'),
|
||||
emphasize: this._view.is('clips'),
|
||||
});
|
||||
}
|
||||
if (this.config.menu_buttons?.snapshots ?? true) {
|
||||
buttons.set('snapshots', {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
card_action: 'snapshots',
|
||||
title: localize('menu.snapshots'),
|
||||
icon: 'mdi:camera',
|
||||
description: localize('menu.snapshots'),
|
||||
emphasize: this._view.is('snapshots'),
|
||||
});
|
||||
}
|
||||
if ((this.config.menu_buttons?.frigate_ui ?? true) && this.config.frigate_url) {
|
||||
buttons.set('frigate_ui', {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
card_action: 'frigate_ui',
|
||||
title: localize('menu.frigate_ui'),
|
||||
icon: 'mdi:web',
|
||||
description: localize('menu.frigate_ui'),
|
||||
});
|
||||
}
|
||||
if ((this.config.menu_buttons?.fullscreen ?? true) && screenfull.isEnabled) {
|
||||
buttons.set('fullscreen', {
|
||||
buttons.push({
|
||||
type: 'internal-menu-icon',
|
||||
card_action: 'fullscreen',
|
||||
title: localize('menu.fullscreen'),
|
||||
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
||||
description: localize('menu.fullscreen'),
|
||||
});
|
||||
}
|
||||
|
||||
const entities = this.config.entities || [];
|
||||
for (let i = 0; this._hass && i < entities.length; i++) {
|
||||
if (!entities[i].show) {
|
||||
continue;
|
||||
const elements = this.config.elements || [];
|
||||
for (let i = 0; this._hass && i < elements.length; i++) {
|
||||
const element = elements[i];
|
||||
if (['menu-icon', 'menu-state-icon'].includes(element.type)) {
|
||||
buttons.push(element);
|
||||
}
|
||||
const entity = entities[i].entity;
|
||||
const state = this._hass.states[entity];
|
||||
buttons.set(entity, {
|
||||
description: state.attributes.friendly_name || entity,
|
||||
emphasize: ['on', 'active', 'home'].includes(state.state),
|
||||
icon: entities[i].icon || stateIcon(state),
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
@@ -285,7 +266,7 @@ export class FrigateCard extends LitElement {
|
||||
|
||||
this.config = config;
|
||||
this._entitiesToMonitor = [
|
||||
...(this.config.entities || []).map((entity) => entity.entity),
|
||||
...(this.config.update_entities || []),
|
||||
this.config.camera_entity,
|
||||
];
|
||||
this._changeView();
|
||||
@@ -328,15 +309,20 @@ export class FrigateCard extends LitElement {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected _menuActionHandler(name: string): void {
|
||||
switch (name) {
|
||||
protected _menuActionHandler(action: string, button: MenuButton): void {
|
||||
if (button.type != 'internal-menu-icon') {
|
||||
handleAction(this, this._hass as HomeAssistant, button, action);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (button.card_action) {
|
||||
case 'frigate':
|
||||
this._changeView();
|
||||
break;
|
||||
case 'live':
|
||||
case 'clips':
|
||||
case 'snapshots':
|
||||
this._changeView(new View({ view: name }));
|
||||
this._changeView(new View({ view: button.card_action }));
|
||||
break;
|
||||
case 'frigate_ui':
|
||||
const frigate_url = this._getFrigateURLFromContext();
|
||||
@@ -350,8 +336,7 @@ export class FrigateCard extends LitElement {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// If it's unknown, it's assumed to be an entity_id.
|
||||
fireEvent(this, 'hass-more-info', { entityId: name });
|
||||
console.warn(`Frigate card received unknown menu action: ${button.card_action}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +374,7 @@ export class FrigateCard extends LitElement {
|
||||
return html`
|
||||
<frigate-card-menu
|
||||
class="${classMap(classes)}"
|
||||
.hass=${this._hass}
|
||||
.actionCallback=${this._menuActionHandler.bind(this)}
|
||||
.menuMode=${this.config.menu_mode}
|
||||
.buttons=${this._getMenuButtons()}
|
||||
@@ -524,7 +510,9 @@ export class FrigateCard extends LitElement {
|
||||
window.innerWidth / window.innerHeight
|
||||
) {
|
||||
// If the menu is outside the media (i.e. above/below) allow space for it.
|
||||
const allowance = ["above", "below"].includes(this.config.menu_mode) ? MENU_HEIGHT : 0;
|
||||
const allowance = ['above', 'below'].includes(this.config.menu_mode)
|
||||
? MENU_HEIGHT
|
||||
: 0;
|
||||
innerStyle['max-width'] = `calc(${
|
||||
(100 * this._mediaInfo.width) / this._mediaInfo.height
|
||||
}vh - ${allowance}px )`;
|
||||
|
||||
+35
-4
@@ -108,14 +108,17 @@ export function dispatchEvent<T>(element: HTMLElement, name: string, detail?: T)
|
||||
}
|
||||
|
||||
export function dispatchPlayEvent(element: HTMLElement): void {
|
||||
dispatchEvent(element, 'play')
|
||||
dispatchEvent(element, 'play');
|
||||
}
|
||||
|
||||
export function dispatchPauseEvent(element: HTMLElement): void {
|
||||
dispatchEvent(element, 'pause')
|
||||
dispatchEvent(element, 'pause');
|
||||
}
|
||||
|
||||
export function dispatchMediaLoadEvent(element: HTMLElement, source: Event | HTMLElement): void {
|
||||
export function dispatchMediaLoadEvent(
|
||||
element: HTMLElement,
|
||||
source: Event | HTMLElement,
|
||||
): void {
|
||||
let target: HTMLElement | EventTarget;
|
||||
if (source instanceof Event) {
|
||||
target = source.composedPath()[0];
|
||||
@@ -139,4 +142,32 @@ export function dispatchMediaLoadEvent(element: HTMLElement, source: Event | HTM
|
||||
height: (target as HTMLCanvasElement).height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the card should be updated based on Home Assistant changes.
|
||||
export function shouldUpdateBasedOnHass(
|
||||
newHass: HomeAssistant | null,
|
||||
oldHass: HomeAssistant | undefined,
|
||||
entities: string[] | null,
|
||||
): boolean {
|
||||
if (!newHass || !entities) {
|
||||
return false;
|
||||
}
|
||||
if (!entities.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oldHass) {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const entity = entities[i];
|
||||
if (!entity) {
|
||||
continue;
|
||||
}
|
||||
if (oldHass.states[entity] !== newHass.states[entity]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+78
-24
@@ -1,19 +1,26 @@
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { HomeAssistant, hasAction, stateIcon } from 'custom-card-helpers';
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit';
|
||||
import { actionHandler } from '../action-handler-directive';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { MenuButton } from '../types';
|
||||
import type { FrigateMenuMode } from '../types';
|
||||
import type { ExtendedHomeAssistant, FrigateMenuMode } from '../types';
|
||||
|
||||
import menuStyle from '../scss/menu.scss';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { shouldUpdateBasedOnHass } from '../common';
|
||||
|
||||
type FrigateCardMenuCallback = (name: string) => void;
|
||||
type FrigateCardMenuCallback = (name: string, button: MenuButton) => void;
|
||||
|
||||
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';
|
||||
|
||||
@@ -24,12 +31,12 @@ export class FrigateCardMenu extends LitElement {
|
||||
protected actionCallback: FrigateCardMenuCallback | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public buttons: Map<string, MenuButton> = new Map();
|
||||
public buttons: MenuButton[] = [];
|
||||
|
||||
// Call the callback.
|
||||
protected _callAction(name: string): void {
|
||||
protected _callAction(ev: CustomEvent, button: MenuButton): void {
|
||||
if (this.menuMode.startsWith('hidden-')) {
|
||||
if (name == 'frigate') {
|
||||
if (button.type == 'internal-menu-icon' && button.card_action === 'frigate') {
|
||||
this.expand = !this.expand;
|
||||
return;
|
||||
}
|
||||
@@ -38,42 +45,93 @@ export class FrigateCardMenu extends LitElement {
|
||||
}
|
||||
|
||||
if (this.actionCallback) {
|
||||
this.actionCallback(name);
|
||||
this.actionCallback(ev.detail.action, button);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the menu should be updated.
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
|
||||
|
||||
if (!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 == 'menu-state-icon') {
|
||||
entities.push(button.entity);
|
||||
}
|
||||
}
|
||||
return shouldUpdateBasedOnHass(this.hass, oldHass, entities);
|
||||
}
|
||||
|
||||
// Render a menu button.
|
||||
protected _renderButton(name: string, button: MenuButton): TemplateResult {
|
||||
protected _renderButton(button: MenuButton): TemplateResult {
|
||||
let state: HassEntity | null = null;
|
||||
let emphasize = false;
|
||||
let title = button.title;
|
||||
let icon = button.icon;
|
||||
|
||||
if (button.type === '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: button.emphasize ?? false,
|
||||
emphasize: emphasize,
|
||||
};
|
||||
|
||||
return html` <ha-icon-button
|
||||
class="${classMap(classes)}"
|
||||
icon=${button.icon || 'mdi:gesture-tap-button'}
|
||||
title=${button.description}
|
||||
@click=${() => this._callAction(name)}
|
||||
style="${styleMap(button.style || {})}"
|
||||
icon=${icon || 'mdi:gesture-tap-button'}
|
||||
title=${title || ''}
|
||||
@action=${(ev) => this._callAction(ev, button)}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasHold,
|
||||
hasDoubleClick: hasDoubleClick,
|
||||
})}
|
||||
></ha-icon-button>`;
|
||||
}
|
||||
|
||||
// Render the Frigate menu button.
|
||||
protected _renderFrigateButton(name: string, button: MenuButton): TemplateResult {
|
||||
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(name, Object.assign({}, button, { icon: icon }));
|
||||
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.card_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.get('frigate'))
|
||||
(this.menuMode.startsWith('hidden-') && !this.buttons.find(isFrigateButton))
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
@@ -103,14 +161,10 @@ export class FrigateCardMenu extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class=${classMap(classes)}>
|
||||
${Array.from(this.buttons.keys()).map((name) => {
|
||||
const button = this.buttons.get(name);
|
||||
if (button) {
|
||||
return name === 'frigate'
|
||||
? this._renderFrigateButton(name, button)
|
||||
: this._renderButton(name, button);
|
||||
}
|
||||
return html``;
|
||||
${Array.from(this.buttons).map((button) => {
|
||||
return isFrigateButton(button)
|
||||
? this._renderFrigateButton(button)
|
||||
: this._renderButton(button);
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
|
||||
+117
-15
@@ -1,4 +1,12 @@
|
||||
import { LovelaceCard, LovelaceCardEditor } from 'custom-card-helpers';
|
||||
import {
|
||||
CallServiceActionConfig,
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
MoreInfoActionConfig,
|
||||
NavigateActionConfig,
|
||||
ToggleActionConfig,
|
||||
UrlActionConfig,
|
||||
} from 'custom-card-helpers';
|
||||
import { z } from 'zod';
|
||||
|
||||
declare global {
|
||||
@@ -46,6 +54,112 @@ export type NextPreviousControlStyle = typeof NEXT_PREVIOUS_CONTROL_STYLES[numbe
|
||||
export const LIVE_PROVIDERS = ['frigate', 'frigate-jsmpeg', 'webrtc'] as const;
|
||||
export type LiveProvider = typeof LIVE_PROVIDERS[number];
|
||||
|
||||
/**
|
||||
* Action Types (for "Picture Elements" / Menu)
|
||||
*/
|
||||
|
||||
// Declare schemas to existing types:
|
||||
// - https://github.com/colinhacks/zod/issues/372#issuecomment-826380330
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const schemaForType =
|
||||
<T>() =>
|
||||
<S extends z.ZodType<T, any, any>>(arg: S) => {
|
||||
return arg;
|
||||
};
|
||||
const toggleActionSchema = schemaForType<ToggleActionConfig>()(
|
||||
z.object({
|
||||
action: z.literal('toggle'),
|
||||
}),
|
||||
);
|
||||
const callServiceActionSchema = schemaForType<CallServiceActionConfig>()(
|
||||
z.object({
|
||||
action: z.literal('call-service'),
|
||||
service: z.string(),
|
||||
service_data: z.object({}).passthrough().optional(),
|
||||
}),
|
||||
);
|
||||
const navigateActionSchema = schemaForType<NavigateActionConfig>()(
|
||||
z.object({
|
||||
action: z.literal('navigate'),
|
||||
navigation_path: z.string(),
|
||||
}),
|
||||
);
|
||||
const urlActionSchema = schemaForType<UrlActionConfig>()(
|
||||
z.object({
|
||||
action: z.literal('url'),
|
||||
url_path: z.string(),
|
||||
}),
|
||||
);
|
||||
const moreInfoActionSchema = schemaForType<MoreInfoActionConfig>()(
|
||||
z.object({
|
||||
action: z.literal('more-info'),
|
||||
}),
|
||||
);
|
||||
const elementsActionSchema = z.union([
|
||||
toggleActionSchema,
|
||||
callServiceActionSchema,
|
||||
navigateActionSchema,
|
||||
urlActionSchema,
|
||||
moreInfoActionSchema,
|
||||
]);
|
||||
export type ElementsActionType = z.infer<typeof elementsActionSchema>;
|
||||
|
||||
const elementsActionsSchema = z.object({
|
||||
tap_action: elementsActionSchema.optional(),
|
||||
hold_action: elementsActionSchema.optional(),
|
||||
double_tap_action: elementsActionSchema.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Menu Types
|
||||
*/
|
||||
|
||||
const menuItemBaseSchema = z.object({
|
||||
title: z.string().optional(),
|
||||
style: z.object({}).passthrough().optional(),
|
||||
});
|
||||
|
||||
const menuIconSchema = menuItemBaseSchema
|
||||
.merge(
|
||||
z.object({
|
||||
type: z.literal('menu-icon'),
|
||||
icon: z.string(),
|
||||
}),
|
||||
)
|
||||
.merge(elementsActionsSchema);
|
||||
|
||||
const menuStateIconSchema = menuItemBaseSchema
|
||||
.merge(
|
||||
z.object({
|
||||
type: z.literal('menu-state-icon'),
|
||||
entity: z.string(),
|
||||
icon: z.string().optional(),
|
||||
state_color: z.boolean().default(true),
|
||||
}),
|
||||
)
|
||||
.merge(elementsActionsSchema);
|
||||
|
||||
// Schema for card (non-user configured) menu icons.
|
||||
const internalMenuIconSchema = menuItemBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.literal('internal-menu-icon'),
|
||||
icon: z.string().optional(),
|
||||
emphasize: z.boolean().default(false).optional(),
|
||||
card_action: z.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
const menuButtonSchema = z.union([
|
||||
menuIconSchema,
|
||||
menuStateIconSchema,
|
||||
internalMenuIconSchema,
|
||||
]);
|
||||
export type MenuButton = z.infer<typeof menuButtonSchema>;
|
||||
|
||||
// 'internalMenuIconSchema' is excluded to disallow the user from manually
|
||||
// changing the internal menu buttons.
|
||||
const elementsSchema = z.union([menuStateIconSchema, menuIconSchema]);
|
||||
|
||||
export const frigateCardConfigSchema = z.object({
|
||||
camera_entity: z.string(),
|
||||
// No URL validation to allow relative URLs within HA (e.g. addons).
|
||||
@@ -85,14 +199,8 @@ export const frigateCardConfigSchema = z.object({
|
||||
fullscreen: z.boolean().default(true),
|
||||
})
|
||||
.optional(),
|
||||
entities: z
|
||||
.object({
|
||||
entity: z.string(),
|
||||
show: z.boolean().default(true),
|
||||
icon: z.string().optional(),
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
update_entities: z.string().array().optional(),
|
||||
elements: elementsSchema.array().optional(),
|
||||
controls: z
|
||||
.object({
|
||||
nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
|
||||
@@ -125,12 +233,6 @@ export const frigateCardConfigSchema = z.object({
|
||||
});
|
||||
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
|
||||
|
||||
export interface MenuButton {
|
||||
icon?: string;
|
||||
description: string;
|
||||
emphasize?: boolean;
|
||||
}
|
||||
|
||||
export interface ExtendedHomeAssistant {
|
||||
hassUrl(path?): string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user