Merge pull request #526 from dermotduffy/menu-priority

Improve menu versatility: support ordering and alignment per button
This commit is contained in:
Dermot Duffy
2022-04-29 20:50:17 -07:00
committed by GitHub
10 changed files with 613 additions and 342 deletions
+41 -9
View File
@@ -184,19 +184,31 @@ All configuration is under:
```yaml
menu:
buttons:
[button]:
```
##### Buttons
| Option | Overridable | Description |
| - | - | - |
| `frigate` | :white_check_mark: | The `Frigate` menu button: brings the user to the default configured view (`view.default`), or collapses/expands the menu if the `menu.style` is `hidden` . |
| `cameras` | :white_check_mark: | The camera selection submenu. Will only appear if multiple cameras are configured. |
| `live` | :white_check_mark: | The `live` view menu button: brings the user to the `live` view. See [views](#views) below.|
| `clips` | :white_check_mark: | The `clips` view menu button: brings the user to the `clips` view on tap and the most-recent `clip` view on hold. See [views](#views) below. This button will never be shown if the `camera_name` for the selected camera is not auto-detected/specified (e.g. non-Frigate cameras), or if the `camera_name` is `birdseye`.|
| `snapshots` | :white_check_mark: | The `snapshots` view menu button: brings the user to the `clips` view on tap and the most-recent `snapshot` view on hold. See [views](#views) below. This button will never be shown if the `camera_name` for the selected camera is not auto-detected/specified (e.g. non-Frigate cameras), or if the `camera_name` is `birdseye`.|
| `image` | :white_check_mark: | The `image` view menu button: brings the user to the static `image` view. See [views](#views) below.|
| `download` | :white_check_mark: | The `download` menu button: allow direct download of the media being displayed.|
| `frigate_ui` | :white_check_mark: | The `frigate_ui` menu button: brings the user to a context-appropriate page on the Frigate UI (e.g. the camera homepage). Will only appear if the `frigate.url` option is set.|
| `fullscreen` | :white_check_mark: | The `fullscreen` menu button: expand the card to consume the fullscreen. |
##### Configuration on each button
| Option | Default | Overridable | Description |
| - | - | - | - |
| `frigate` | `true` | :white_check_mark: | Whether to show the `Frigate` menu button: brings the user to the default configured view (`view.default`), or collapses/expands the menu if the `menu.style` is `hidden` . |
| `cameras` | `true` | :white_check_mark: | Whether to show the camera selection submenu. Will only appear if multiple cameras are configured. |
| `live` | `true` | :white_check_mark: | Whether to show the `live` view menu button: brings the user to the `live` view. See [views](#views) below.|
| `clips` | `true` | :white_check_mark: | Whether to show the `clips` view menu button: brings the user to the `clips` view on tap and the most-recent `clip` view on hold. See [views](#views) below. This button will never be shown if the `camera_name` for the selected camera is not auto-detected/specified (e.g. non-Frigate cameras), or if the `camera_name` is `birdseye`.|
| `snapshots` | `true` | :white_check_mark: | Whether to show the `snapshots` view menu button: brings the user to the `clips` view on tap and the most-recent `snapshot` view on hold. See [views](#views) below. This button will never be shown if the `camera_name` for the selected camera is not auto-detected/specified (e.g. non-Frigate cameras), or if the `camera_name` is `birdseye`.|
| `image` | `false` | :white_check_mark: | Whether to show the `image` view menu button: brings the user to the static `image` view. See [views](#views) below.|
| `download` | `true` | :white_check_mark: | Whether to show the `download` menu button: allow direct download of the media being displayed.|
| `frigate_ui` | `true` | :white_check_mark: | Whether to show the `frigate_ui` menu button: brings the user to a context-appropriate page on the Frigate UI (e.g. the camera homepage). Will only appear if the `frigate.url` option is set.|
| `fullscreen` | `true` | :white_check_mark: | Whether to show the `fullscreen` menu button: expand the card to consume the fullscreen. |
| `enabled` | `true` for all buttons except `image` | :white_check_mark: | Whether or not to show the button. |
| `priority` | `50` | :white_check_mark: | The button priority. Higher priority buttons are ordered closer to the start of the menu alignment (i.e. a button with priority `70` will order further to the left than a button with priority `60`, when the menu alignment is `left`). Minimum `0`, maximum `100`.|
| `icon` | | :white_check_mark: | An icon to overriding the default for that button, e.g. `mdi:camera-front`. |
| `alignment` | `matching` | :white_check_mark: | Whether this button should have an alignment that is `matching` the menu alignment or `opposing` the menu. Can be used to create two separate groups of buttons on the menu. `priority` orders buttons within a given `alignment`. |
### Live Options
@@ -1289,6 +1301,26 @@ card_mod:
```
</details>
### Overriding default menu behavior
<details>
<summary>Expand: Overriding default menu behavior</summary>
This example moves the fullscreen button into its own group aligned to the `left`, enables the `image` button and orders it furthest to the `right`.
```yaml
[...]
menu:
alignment: right
buttons:
image:
enabled: true
priority: 100
fullscreen:
alignment: opposing
```
</details>
### Using a dependent camera
`dependent_cameras` allows events for other cameras to be shown along with the currently selected camera. For example, this can be used to show events with the `birdseye` camera (since it will not have events of its own).
+57 -58
View File
@@ -278,25 +278,19 @@ export class FrigateCard extends LitElement {
protected _getMenuButtons(): MenuButton[] {
const buttons: MenuButton[] = [];
if (this._getConfig().menu.buttons.frigate) {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.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,
tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu)
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
: (createFrigateCardCustomAction('default') as FrigateCardCustomAction),
});
}
buttons.push({
// Use a magic icon value that the menu will use to render the custom
// Frigate icon.
icon: FRIGATE_BUTTON_MENU_ICON,
...this._getConfig().menu.buttons.frigate,
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.frigate'),
tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu)
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
: (createFrigateCardCustomAction('default') as FrigateCardCustomAction),
});
if (
this._getConfig().menu.buttons.cameras &&
this._cameras &&
this._cameras.size > 1
) {
if (this._cameras && this._cameras.size > 1) {
const menuItems = Array.from(this._cameras, ([camera, config]) => {
return {
icon: getCameraIcon(this._hass, config),
@@ -309,37 +303,37 @@ export class FrigateCard extends LitElement {
});
buttons.push({
icon: 'mdi:video-switch',
...this._getConfig().menu.buttons.cameras,
type: 'custom:frigate-card-menu-submenu',
title: localize('config.menu.buttons.cameras'),
icon: 'mdi:video-switch',
items: menuItems,
});
}
if (this._getConfig().menu.buttons.live) {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.live'),
icon: 'mdi:cctv',
style: this._view?.is('live') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
});
}
buttons.push({
icon: 'mdi:cctv',
...this._getConfig().menu.buttons.live,
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.live'),
style: this._view?.is('live') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
});
const cameraConfig = this._getSelectedCameraConfig();
// Don't show `clips` button if there's no `camera_name` (e.g. non-Frigate
// cameras), or is birdseye (unless there are dependent cameras).
if (
this._getConfig().menu.buttons.clips &&
cameraConfig?.camera_name &&
(cameraConfig?.camera_name !== CAMERA_BIRDSEYE ||
cameraConfig?.dependent_cameras?.length)
) {
buttons.push({
icon: 'mdi:filmstrip',
...this._getConfig().menu.buttons.clips,
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.clips'),
icon: 'mdi:filmstrip',
style: this._view?.is('clips') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
hold_action: createFrigateCardCustomAction('clip') as FrigateCardCustomAction,
@@ -349,15 +343,15 @@ export class FrigateCard extends LitElement {
// Don't show `snapshots` button if there's no `camera_name` (e.g. non-Frigate
// cameras), or is birdseye (unless there are dependent cameras).
if (
this._getConfig().menu.buttons.snapshots &&
cameraConfig?.camera_name &&
(cameraConfig?.camera_name !== CAMERA_BIRDSEYE ||
cameraConfig?.dependent_cameras?.length)
) {
buttons.push({
icon: 'mdi:camera',
...this._getConfig().menu.buttons.snapshots,
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.snapshots'),
icon: 'mdi:camera',
style: this._view?.is('snapshots') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction(
'snapshots',
@@ -368,54 +362,55 @@ export class FrigateCard extends LitElement {
});
}
if (this._getConfig().menu.buttons.image) {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.image'),
icon: 'mdi:image',
style: this._view?.is('image') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
});
}
buttons.push({
icon: 'mdi:image',
...this._getConfig().menu.buttons.image,
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.image'),
style: this._view?.is('image') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
});
if (this._getConfig().menu.buttons.timeline) {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.timeline'),
icon: 'mdi:chart-gantt',
style: this._view?.is('timeline') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction,
});
}
buttons.push({
icon: 'mdi:chart-gantt',
...this._getConfig().menu.buttons.timeline,
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.timeline'),
style: this._view?.is('timeline') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction,
});
if (
this._getConfig().menu.buttons.download &&
(this._view?.isViewerView() || (this._view?.is('timeline') && !!this._view?.media))
this._view?.isViewerView() ||
(this._view?.is('timeline') && !!this._view?.media)
) {
buttons.push({
icon: 'mdi:download',
...this._getConfig().menu.buttons.download,
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.download'),
icon: 'mdi:download',
tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
});
}
if (this._getConfig().menu.buttons.frigate_ui && cameraConfig?.frigate_url) {
if (cameraConfig?.frigate_url) {
buttons.push({
icon: 'mdi:web',
...this._getConfig().menu.buttons.frigate_ui,
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.frigate_ui'),
icon: 'mdi:web',
tap_action: createFrigateCardCustomAction(
'frigate_ui',
) as FrigateCardCustomAction,
});
}
if (this._getConfig().menu.buttons.fullscreen && screenfull.isEnabled) {
if (screenfull.isEnabled) {
buttons.push({
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
...this._getConfig().menu.buttons.fullscreen,
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.fullscreen'),
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
tap_action: createFrigateCardCustomAction(
'fullscreen',
) as FrigateCardCustomAction,
@@ -489,7 +484,11 @@ export class FrigateCard extends LitElement {
};
if (this._getConfig().cameras && Array.isArray(this._getConfig().cameras)) {
await Promise.all(this._getConfig().cameras.map(addCameraConfig.bind(this)));
// Cameras are loaded sequentially rather than in parallel to preserve the
// order of the input camera array.
for (const camera of this._getConfig().cameras) {
await addCameraConfig(camera);
}
}
if (!cameras.size) {
+72 -4
View File
@@ -1,4 +1,11 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import {
CSSResultGroup,
LitElement,
TemplateResult,
html,
unsafeCSS,
PropertyValues,
} from 'lit';
import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -14,6 +21,7 @@ import type {
ActionType,
MenuButton,
MenuConfig,
MenuItem,
StateParameters,
} from '../types.js';
import {
@@ -198,12 +206,52 @@ export class FrigateCardMenu extends LitElement {
}
}
/**
* Ensure menu buttons are sorted before the render.
* @param changedProps The changed properties
*/
protected willUpdate(changedProps: PropertyValues): void {
const style = this._menuConfig?.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;
};
if (changedProps.has('_menuConfig') || changedProps.has('buttons')) {
this.buttons.sort(sortButtons);
}
}
/**
* Render a button.
* @param button The button configuration to render.
* @returns A rendered template or void.
*/
protected _renderButton(button: MenuButton): TemplateResult | void {
if (button.enabled === false) {
return;
}
if (button.type == 'custom:frigate-card-menu-submenu') {
return html` <frigate-card-submenu
.hass=${this.hass}
@@ -274,11 +322,31 @@ export class FrigateCardMenu extends LitElement {
}
// If the hidden menu isn't expanded, only show the Frigate button.
const buttons =
const matchingButtons =
style !== 'hidden' || this.expanded
? this.buttons
? this.buttons.filter(
(button) => !button.alignment || button.alignment === 'matching',
)
: this.buttons.filter((button) => button.icon === FRIGATE_BUTTON_MENU_ICON);
return html` ${buttons.map((button) => this._renderButton(button))} `;
const opposingButtons =
style !== 'hidden' || this.expanded
? this.buttons.filter((button) => button.alignment === 'opposing')
: [];
const matchingStyle = {
flex: String(matchingButtons.length),
};
const opposingStyle = {
flex: String(opposingButtons.length),
};
return html` <div class="matching" style="${styleMap(matchingStyle)}">
${matchingButtons.map((button) => this._renderButton(button))}
</div>
<div class="opposing" style="${styleMap(opposingStyle)}">
${opposingButtons.map((button) => this._renderButton(button))}
</div>`;
}
/**
+36
View File
@@ -18,6 +18,15 @@ import {
CONF_LIVE_PRELOAD,
CONF_LIVE_WEBRTC_CARD,
CONF_MENU,
CONF_MENU_BUTTONS_FRIGATE,
CONF_MENU_BUTTONS_CAMERAS,
CONF_MENU_BUTTONS_LIVE,
CONF_MENU_BUTTONS_CLIPS,
CONF_MENU_BUTTONS_SNAPSHOTS,
CONF_MENU_BUTTONS_IMAGE,
CONF_MENU_BUTTONS_DOWNLOAD,
CONF_MENU_BUTTONS_FRIGATE_UI,
CONF_MENU_BUTTONS_FULLSCREEN,
CONF_MENU_BUTTON_SIZE,
CONF_MENU_POSITION,
CONF_MENU_STYLE,
@@ -476,6 +485,24 @@ const upgradeMenuConditionToMenuOverride = (): ((
};
};
/**
* Transform a menu button from a boolean to a priority.
* @param value The boolean true/false for show/hide the switch.
* @returns A priority value.
*/
const menuButtonBooleanToObject = function (
value: unknown,
): { enabled: boolean } | null | undefined {
if (typeof value === 'object') {
return undefined;
}
// If it's not a boolean, or it's just true, remove it.
if (typeof value !== 'boolean' || value) {
return null;
}
return { enabled: value };
};
const UPGRADES = [
// v1.2.1 -> v2.0.0
upgradeMoveTo('frigate_url', 'frigate.url'),
@@ -539,4 +566,13 @@ const UPGRADES = [
),
upgradeWithOverrides('event_gallery.min_columns', deleteProperty),
upgradeMenuModeToStyleAndPosition(),
upgradeWithOverrides(CONF_MENU_BUTTONS_FRIGATE, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_CAMERAS, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_LIVE, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_CLIPS, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_SNAPSHOTS, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_IMAGE, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_DOWNLOAD, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_FRIGATE_UI, menuButtonBooleanToObject),
upgradeWithOverrides(CONF_MENU_BUTTONS_FULLSCREEN, menuButtonBooleanToObject),
];
+26 -20
View File
@@ -17,7 +17,8 @@ export const CONF_CAMERAS_ARRAY_TITLE = `${CONF_CAMERAS}.#.title` as const;
export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const;
export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY =
`${CONF_CAMERAS}.#.webrtc_card.entity` as const;
export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL = `${CONF_CAMERAS}.#.webrtc_card.url` as const;
export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL =
`${CONF_CAMERAS}.#.webrtc_card.url` as const;
export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER =
`${CONF_CAMERAS}.#.live_provider` as const;
export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS =
@@ -35,9 +36,9 @@ export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_EVENT_GALLERY}.controls.thumbnails.show_details` as const;
`${CONF_EVENT_GALLERY}.controls.thumbnails.show_details` as const;
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
`${CONF_EVENT_GALLERY}.controls.thumbnails.show_controls` as const;
`${CONF_EVENT_GALLERY}.controls.thumbnails.show_controls` as const;
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE =
`${CONF_EVENT_GALLERY}.controls.thumbnails.size` as const;
@@ -55,9 +56,9 @@ export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE =
`${CONF_EVENT_VIEWER}.controls.thumbnails.mode` as const;
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_EVENT_VIEWER}.controls.thumbnails.show_details` as const;
`${CONF_EVENT_VIEWER}.controls.thumbnails.show_details` as const;
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
`${CONF_EVENT_VIEWER}.controls.thumbnails.show_controls` as const;
`${CONF_EVENT_VIEWER}.controls.thumbnails.show_controls` as const;
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE =
`${CONF_EVENT_VIEWER}.controls.thumbnails.size` as const;
export const CONF_EVENT_VIEWER_CONTROLS_TITLE_MODE =
@@ -78,9 +79,9 @@ export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE =
export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE =
`${CONF_LIVE}.controls.thumbnails.size` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_LIVE}.controls.thumbnails.show_details` as const;
`${CONF_LIVE}.controls.thumbnails.show_details` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
`${CONF_LIVE}.controls.thumbnails.show_controls` as const;
`${CONF_LIVE}.controls.thumbnails.show_controls` as const;
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_LIVE}.controls.title.duration_seconds` as const;
@@ -99,29 +100,34 @@ export const CONF_IMAGE_URL = `${CONF_IMAGE}.url` as const;
export const CONF_TIMELINE = 'timeline' as const;
export const CONF_TIMELINE_WINDOW_SECONDS = `${CONF_TIMELINE}.window_seconds` as const;
export const CONF_TIMELINE_CLUSTERING_THRESHOLD = `${CONF_TIMELINE}.clustering_threshold` as const;
export const CONF_TIMELINE_CLUSTERING_THRESHOLD =
`${CONF_TIMELINE}.clustering_threshold` as const;
export const CONF_TIMELINE_MEDIA = `${CONF_TIMELINE}.media` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE = `${CONF_TIMELINE}.controls.thumbnails.mode` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE = `${CONF_TIMELINE}.controls.thumbnails.size` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS = `${CONF_TIMELINE}.controls.thumbnails.show_details` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE =
`${CONF_TIMELINE}.controls.thumbnails.mode` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE =
`${CONF_TIMELINE}.controls.thumbnails.size` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_TIMELINE}.controls.thumbnails.show_details` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
`${CONF_TIMELINE}.controls.thumbnails.show_controls` as const;
`${CONF_TIMELINE}.controls.thumbnails.show_controls` as const;
export const CONF_MENU = 'menu' as const;
export const CONF_MENU_ALIGNMENT = `${CONF_MENU}.alignment` as const;
export const CONF_MENU_POSITION = `${CONF_MENU}.position` as const;
export const CONF_MENU_STYLE = `${CONF_MENU}.style` as const;
export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const;
export const CONF_MENU_BUTTONS = `${CONF_MENU}.buttons` as const;
export const CONF_MENU_BUTTONS_CAMERAS = `${CONF_MENU}.buttons.cameras` as const;
export const CONF_MENU_BUTTONS_CLIPS = `${CONF_MENU}.buttons.clips` as const;
export const CONF_MENU_BUTTONS_DOWNLOAD = `${CONF_MENU}.buttons.download` as const;
export const CONF_MENU_BUTTONS_FRIGATE = `${CONF_MENU}.buttons.frigate` as const;
export const CONF_MENU_BUTTONS_FRIGATE_UI = `${CONF_MENU}.buttons.frigate_ui` as const;
export const CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN =
`${CONF_MENU}.buttons.fullscreen` as const;
export const CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD =
`${CONF_MENU}.buttons.download` as const;
export const CONF_MENU_BUTTONS_LIVE = `${CONF_MENU}.buttons.live` as const;
export const CONF_MENU_BUTTONS_CLIPS = `${CONF_MENU}.buttons.clips` as const;
export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const;
export const CONF_MENU_BUTTONS_FULLSCREEN = `${CONF_MENU}.buttons.fullscreen` as const;
export const CONF_MENU_BUTTONS_IMAGE = `${CONF_MENU}.buttons.image` as const;
export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const;
export const CONF_MENU_BUTTONS_LIVE = `${CONF_MENU}.buttons.live` as const;
export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const;
export const CONF_DIMENSIONS = 'dimensions' as const;
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
+210 -169
View File
@@ -6,6 +6,7 @@ import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helper
import { localize } from './localize/localize.js';
import {
BUTTON_SIZE_MIN,
FRIGATE_MENU_PRIORITY_MAX,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX,
@@ -65,14 +66,7 @@ import {
CONF_LIVE_PRELOAD,
CONF_LIVE_TRANSITION_EFFECT,
CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTONS_CLIPS,
CONF_MENU_BUTTONS_FRIGATE,
CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD,
CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN,
CONF_MENU_BUTTONS_FRIGATE_UI,
CONF_MENU_BUTTONS_IMAGE,
CONF_MENU_BUTTONS_LIVE,
CONF_MENU_BUTTONS_SNAPSHOTS,
CONF_MENU_BUTTONS,
CONF_MENU_BUTTON_SIZE,
CONF_MENU_POSITION,
CONF_MENU_STYLE,
@@ -104,89 +98,79 @@ import {
import frigate_card_editor_style from './scss/editor.scss';
const MENU_BUTTONS = 'buttons';
const MENU_CAMERAS = 'cameras';
const MENU_OPTIONS = 'options';
interface EditorOptionsSet {
icon: string;
name: string;
secondary: string;
show: boolean;
}
interface EditorOptions {
[setName: string]: EditorOptionsSet;
}
interface EditorCameraTarget {
cameraIndex: number;
}
interface EditorOptionSetTarget {
optionSetName: string;
}
interface EditorSelectOption {
value: string;
label: string;
}
interface EditorMenuTarget {
domain: string;
key: string | number;
}
const options: EditorOptions = {
cameras: {
icon: 'video',
name: localize('editor.cameras'),
secondary: localize('editor.cameras_secondary'),
show: true,
},
view: {
icon: 'eye',
name: localize('editor.view'),
secondary: localize('editor.view_secondary'),
show: false,
},
menu: {
icon: 'menu',
name: localize('editor.menu'),
secondary: localize('editor.menu_secondary'),
show: false,
},
live: {
icon: 'cctv',
name: localize('editor.live'),
secondary: localize('editor.live_secondary'),
show: false,
},
event_viewer: {
icon: 'filmstrip',
name: localize('editor.event_viewer'),
secondary: localize('editor.event_viewer_secondary'),
show: false,
},
event_gallery: {
icon: 'grid',
name: localize('editor.event_gallery'),
secondary: localize('editor.event_gallery_secondary'),
show: false,
},
image: {
icon: 'image',
name: localize('editor.image'),
secondary: localize('editor.image_secondary'),
show: false,
},
timeline: {
icon: 'chart-gantt',
name: localize('editor.timeline'),
secondary: localize('editor.timeline_secondary'),
show: false,
},
dimensions: {
icon: 'aspect-ratio',
name: localize('editor.dimensions'),
secondary: localize('editor.dimensions_secondary'),
show: false,
},
overrides: {
icon: 'file-replace',
name: localize('editor.overrides'),
secondary: localize('editor.overrides_secondary'),
show: false,
},
};
@@ -199,7 +183,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
protected _configUpgradeable = false;
@property({ attribute: false })
protected _expandedCameraIndex: number | null = null;
protected _expandedMenus: Record<string, string | number> = {};
protected _viewModes: EditorSelectOption[] = [
{ value: '', label: '' },
@@ -408,8 +392,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return html`
<div
class="option option-${optionSetName}"
@click=${this._toggleOptionHandler}
.optionSetName=${optionSetName}
@click=${this._toggleMenu}
.domain=${'options'}
.key=${optionSetName}
>
<div class="row">
<ha-icon .icon=${`mdi:${optionSet.icon}`}></ha-icon>
@@ -465,12 +450,16 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
* Render an option/"select" selector.
* @param configPath The configuration path to set/read.
* @param options The options to show in the selector.
* @param params Option parameters to control the selector.
* @returns A rendered template.
*/
protected _renderOptionSelector(
configPath: string,
options: string[] | { value: string; label: string }[],
multiple?: boolean,
params?: {
multiple?: boolean;
label?: string;
},
): TemplateResult | void {
if (!this._config) {
return;
@@ -480,9 +469,40 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<ha-selector
.hass=${this.hass}
.selector=${{
select: { mode: 'dropdown', multiple: !!multiple, options: options },
select: { mode: 'dropdown', multiple: !!params?.multiple, options: options },
}}
.label=${this._getLabel(configPath)}
.label=${params?.label || this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
</ha-selector>
`;
}
/**
* Render an icon selector.
* @param configPath The configuration path to set/read.
* @param params Optional parameters to control the selector.
* @returns A rendered template.
*/
protected _renderIconSelector(
configPath: string,
params?: {
label?: string;
},
): TemplateResult | void {
if (!this._config) {
return;
}
return html`
<ha-selector
.hass=${this.hass}
.selector=${{
icon: {},
}}
.label=${params?.label || this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
@@ -494,29 +514,30 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
/**
* Render a number slider.
* @param configPath Configuration path of the variable.
* @param valueDefault The default value.
* @param icon The icon to use on the slider.
* @param min The minimum value.
* @param max The maximum value.
* @param params Optional parameters to control the selector.
* @returns A rendered template.
*/
protected _renderNumberInput(
configPath: string,
min?: number,
max?: number,
params?: {
min?: number;
max?: number;
label?: string;
default?: number;
},
): TemplateResult | void {
if (!this._config) {
return;
}
const value = getConfigValue(this._config, configPath);
const mode = max === undefined ? 'box' : 'slider';
const mode = params?.max === undefined ? 'box' : 'slider';
return html`
<ha-selector
.hass=${this.hass}
.selector=${{ number: { min: min || 0, max: max, mode: mode } }}
.label=${this._getLabel(configPath)}
.value=${value}
.selector=${{ number: { min: params?.min || 0, max: params?.max, mode: mode } }}
.label=${params?.label || this._getLabel(configPath)}
.value=${value ?? params?.default}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
>
@@ -549,6 +570,62 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
);
}
/**
* Render an editor menu for the card menu buttons.
* @param button The name of the button.
* @returns A rendered template.
*/
protected _renderMenuButton(button: string): TemplateResult {
const menuButtonAlignments: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'matching', label: localize('config.menu.buttons.alignments.matching') },
{ value: 'opposing', label: localize('config.menu.buttons.alignments.opposing') },
];
return html`
<div
class="submenu-header"
@click=${this._toggleMenu}
.domain=${MENU_BUTTONS}
.key=${button}
>
<ha-icon .icon=${'mdi:gesture-tap-button'}></ha-icon>
<span
>${localize('editor.button') +
': ' +
localize(`config.${CONF_MENU_BUTTONS}.${button}`)}</span
>
</div>
${this._expandedMenus[MENU_BUTTONS] === button
? html` <div class="values">
${this._renderSwitch(
`${CONF_MENU_BUTTONS}.${button}.enabled`,
frigateCardConfigDefaults.menu.buttons[button]?.enabled ?? true,
{
label: localize('config.menu.buttons.enabled'),
},
)}
${this._renderOptionSelector(
`${CONF_MENU_BUTTONS}.${button}.alignment`,
menuButtonAlignments,
{
label: localize('config.menu.buttons.alignment'),
},
)}
${this._renderNumberInput(`${CONF_MENU_BUTTONS}.${button}.priority`, {
max: FRIGATE_MENU_PRIORITY_MAX,
default: frigateCardConfigDefaults.menu.buttons[button]?.priority,
label: localize('config.menu.buttons.priority'),
})}
${this._renderIconSelector(`${CONF_MENU_BUTTONS}.${button}.icon`, {
label: localize('config.menu.buttons.icon'),
})}
</div>`
: ''}
`;
}
/**
* Render a camera header.
* @param cameraIndex The index of the camera to edit/add.
@@ -563,9 +640,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
): TemplateResult {
return html`
<div
class="camera-header"
@click=${this._toggleCameraHandler}
.cameraIndex=${cameraIndex}
class="submenu-header"
@click=${this._toggleMenu}
.domain=${MENU_CAMERAS}
.key=${cameraIndex}
>
<ha-icon .icon=${addNewCamera ? 'mdi:video-plus' : 'mdi:video'}></ha-icon>
<span>
@@ -629,7 +707,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return html`
${this._renderCameraHeader(cameraIndex, cameras[cameraIndex], addNewCamera)}
${this._expandedCameraIndex === cameraIndex
${this._expandedMenus[MENU_CAMERAS] === cameraIndex
? html` <div class="values">
<div class="controls">
<ha-icon-button
@@ -644,7 +722,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
modifyConfig((config: RawFrigateCardConfig): boolean => {
if (Array.isArray(config.cameras) && cameraIndex > 0) {
arrayMove(config.cameras, cameraIndex, cameraIndex - 1);
this._expandedCameraIndex = cameraIndex - 1;
this._openMenu(MENU_CAMERAS, cameraIndex - 1);
return true;
}
return false;
@@ -667,7 +745,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
cameraIndex < config.cameras.length - 1
) {
arrayMove(config.cameras, cameraIndex, cameraIndex + 1);
this._expandedCameraIndex = cameraIndex + 1;
this._openMenu(MENU_CAMERAS, cameraIndex + 1);
return true;
}
return false;
@@ -683,7 +761,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
modifyConfig((config: RawFrigateCardConfig): boolean => {
if (Array.isArray(config.cameras)) {
config.cameras.splice(cameraIndex, 1);
this._expandedCameraIndex = null;
this._closeMenu(MENU_CAMERAS);
return true;
}
return false;
@@ -735,7 +813,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS, cameraIndex),
dependentCameras,
true,
{
multiple: true,
},
)}
</div>`
: ``}
@@ -786,13 +866,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
* Render a boolean selector.
* @param configPath The configuration path to set/read.
* @param valueDefault The default switch value if unset.
* @param label An optional switch label.
* @param params Optional parameters to control the selector.
* @returns A rendered template.
*/
protected _renderSwitch(
configPath: string,
valueDefault: boolean,
label?: string,
params?: {
label?: string;
},
): TemplateResult | void {
if (!this._config) {
return;
@@ -802,7 +884,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<ha-selector
.hass=${this.hass}
.selector=${{ boolean: {} }}
.label=${label || this._getLabel(configPath)}
.label=${params?.label || this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, valueDefault)}
.required=${false}
@value-changed=${(ev) => this._valueChangedHandler(configPath, ev)}
@@ -823,9 +905,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
const defaults = frigateCardConfigDefaults;
const getShowButtonLabel = (configPath: string) =>
localize('editor.show_button') + ': ' + localize(`config.${configPath}`);
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
[]) as RawFrigateCardConfigArray;
@@ -852,14 +931,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
: html``}
<div class="card-config">
${this._renderOptionSetHeader('cameras')}
${options.cameras.show
? html` <div class="cameras">
${this._expandedMenus[MENU_OPTIONS] === 'cameras'
? html` <div class="submenu">
${cameras.map((_, index) => this._renderCamera(cameras, index))}
${this._renderCamera(cameras, cameras.length, true)}
</div>`
: ''}
${this._renderOptionSetHeader('view')}
${options.view.show
${this._expandedMenus[MENU_OPTIONS] === 'view'
? html`
<div class="values">
${this._renderOptionSelector(CONF_VIEW_DEFAULT, this._viewModes)}
@@ -879,58 +958,32 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
`
: ''}
${this._renderOptionSetHeader('menu')}
${options.menu.show
${this._expandedMenus[MENU_OPTIONS] === 'menu'
? html`
<div class="values">
${this._renderOptionSelector(CONF_MENU_STYLE, this._menuStyles)}
${this._renderOptionSelector(CONF_MENU_POSITION, this._menuPositions)}
${this._renderOptionSelector(CONF_MENU_ALIGNMENT, this._menuAlignments)}
${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, BUTTON_SIZE_MIN)}
${this._renderSwitch(
CONF_MENU_BUTTONS_FRIGATE,
defaults.menu.buttons.frigate,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE),
)}
${this._renderSwitch(
CONF_MENU_BUTTONS_LIVE,
defaults.menu.buttons.live,
getShowButtonLabel('view.views.live'),
)}
${this._renderSwitch(
CONF_MENU_BUTTONS_CLIPS,
defaults.menu.buttons.clips,
getShowButtonLabel('view.views.clips'),
)}
${this._renderSwitch(
CONF_MENU_BUTTONS_SNAPSHOTS,
defaults.menu.buttons.snapshots,
getShowButtonLabel('view.views.snapshots'),
)}
${this._renderSwitch(
CONF_MENU_BUTTONS_IMAGE,
defaults.menu.buttons.image,
getShowButtonLabel('view.views.image'),
)}
${this._renderSwitch(
CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD,
defaults.menu.buttons.download,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD),
)}
${this._renderSwitch(
CONF_MENU_BUTTONS_FRIGATE_UI,
defaults.menu.buttons.frigate_ui,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_UI),
)}
${this._renderSwitch(
CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN,
defaults.menu.buttons.fullscreen,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN),
)}
${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, {
min: BUTTON_SIZE_MIN,
})}
</div>
<div class="submenu">
${this._renderMenuButton('frigate')}
${this._renderMenuButton('live')}
${this._renderMenuButton('clips')}
${this._renderMenuButton('snapshots')}
${this._renderMenuButton('image')}
${this._renderMenuButton('download')}
${this._renderMenuButton('frigate_ui')}
${this._renderMenuButton('fullscreen')}
${this._renderMenuButton('timeline')}
</div>
</div>
`
: ''}
${this._renderOptionSetHeader('live')}
${options.live.show
${this._expandedMenus[MENU_OPTIONS] === 'live'
? html`
<div class="values">
${this._renderSwitch(CONF_LIVE_PRELOAD, defaults.live.preload)}
@@ -942,10 +995,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
this._liveNextPreviousControlStyles,
)}
${this._renderNumberInput(
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
BUTTON_SIZE_MIN,
)}
${this._renderNumberInput(CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, {
min: BUTTON_SIZE_MIN,
})}
${this._renderOptionSelector(
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
this._thumbnailModes,
@@ -954,11 +1006,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
this._thumbnailMedias,
)}
${this._renderNumberInput(
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
THUMBNAIL_WIDTH_MIN,
THUMBNAIL_WIDTH_MAX,
)}
${this._renderNumberInput(CONF_LIVE_CONTROLS_THUMBNAILS_SIZE, {
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
})}
${this._renderOptionSelector(
CONF_LIVE_CONTROLS_TITLE_MODE,
this._titleModes,
@@ -971,11 +1022,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
defaults.live.controls.thumbnails.show_controls,
)}
${this._renderNumberInput(
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
0,
60,
)}
${this._renderNumberInput(CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS, {
min: 0,
max: 60,
})}
${this._renderOptionSelector(
CONF_LIVE_TRANSITION_EFFECT,
this._transitionEffects,
@@ -984,13 +1034,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
`
: ''}
${this._renderOptionSetHeader('event_gallery')}
${options.event_gallery.show
${this._expandedMenus[MENU_OPTIONS] === 'event_gallery'
? html` <div class="values">
${this._renderNumberInput(
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE,
THUMBNAIL_WIDTH_MIN,
THUMBNAIL_WIDTH_MAX,
)}
${this._renderNumberInput(CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE, {
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
})}
${this._renderSwitch(
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
defaults.event_viewer.controls.thumbnails.show_details,
@@ -1002,7 +1051,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>`
: ''}
${this._renderOptionSetHeader('event_viewer')}
${options.event_viewer.show
${this._expandedMenus[MENU_OPTIONS] === 'event_viewer'
? html` <div class="values">
${this._renderSwitch(
CONF_EVENT_VIEWER_AUTO_PLAY,
@@ -1024,19 +1073,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
this._eventViewerNextPreviousControlStyles,
)}
${this._renderNumberInput(
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
BUTTON_SIZE_MIN,
)}
${this._renderNumberInput(CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, {
min: BUTTON_SIZE_MIN,
})}
${this._renderOptionSelector(
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
this._thumbnailModes,
)}
${this._renderNumberInput(
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
THUMBNAIL_WIDTH_MIN,
THUMBNAIL_WIDTH_MAX,
)}
${this._renderNumberInput(CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, {
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
})}
${this._renderSwitch(
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
defaults.event_viewer.controls.thumbnails.show_details,
@@ -1051,8 +1098,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
${this._renderNumberInput(
CONF_EVENT_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
0,
60,
{ min: 0, max: 60 },
)}
${this._renderOptionSelector(
CONF_EVENT_VIEWER_TRANSITION_EFFECT,
@@ -1061,7 +1107,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>`
: ''}
${this._renderOptionSetHeader('image')}
${options.image.show
${this._expandedMenus[MENU_OPTIONS] === 'image'
? html` <div class="values">
${this._renderOptionSelector(CONF_IMAGE_MODE, this._imageModes)}
${this._renderStringInput(CONF_IMAGE_URL)}
@@ -1069,7 +1115,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>`
: ''}
${this._renderOptionSetHeader('timeline')}
${options.timeline.show
${this._expandedMenus[MENU_OPTIONS] === 'timeline'
? html` <div class="values">
${this._renderNumberInput(CONF_TIMELINE_WINDOW_SECONDS)}
${this._renderNumberInput(CONF_TIMELINE_CLUSTERING_THRESHOLD)}
@@ -1081,11 +1127,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
this._thumbnailModes,
)}
${this._renderNumberInput(
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
THUMBNAIL_WIDTH_MIN,
THUMBNAIL_WIDTH_MAX,
)}
${this._renderNumberInput(CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE, {
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
})}
${this._renderSwitch(
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
defaults.timeline.controls.thumbnails.show_details,
@@ -1097,7 +1142,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>`
: ''}
${this._renderOptionSetHeader('dimensions')}
${options.dimensions.show
${this._expandedMenus[MENU_OPTIONS] === 'dimensions'
? html` <div class="values">
${this._renderOptionSelector(
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
@@ -1108,7 +1153,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
: ''}
${this._config['overrides'] !== undefined
? html` ${this._renderOptionSetHeader('overrides')}
${options.overrides.show
${this._expandedMenus[MENU_OPTIONS] === 'overrides'
? html` <div class="values">
${this._renderInfo(localize('config.overrides.info'))}
</div>`
@@ -1149,42 +1194,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
}
/**
* Display/hide a camera section.
* @param ev The event triggering the change.
* Close the editor menu with the given domain.
* @param targetDomain The menu domain to close.
*/
protected _toggleCameraHandler(ev: { target: EditorCameraTarget | null }): void {
if (ev && ev.target) {
this._expandedCameraIndex =
this._expandedCameraIndex == ev.target.cameraIndex
? null
: ev.target.cameraIndex;
}
protected _closeMenu(targetDomain: string) {
delete this._expandedMenus[targetDomain];
this.requestUpdate();
}
/**
* Handle a toggled set of options.
* @param ev The event triggering the change.
* Open an editor menu.
* @param targetDomain The menu domain to open.
* @param key The menu object key to open.
*/
protected _toggleOptionHandler(ev: { target: EditorOptionSetTarget | null }): void {
this._toggleOptionSet(ev, options);
protected _openMenu(targetDomain: string, key: number | string) {
this._expandedMenus[targetDomain] = key;
this.requestUpdate();
}
/**
* Toggle display of a set of options (e.g. 'Live')
* @param ev The event triggering the change.
* @param options The EditorOptions object.
* Toggle an editor menu.
* @param ev An event.
*/
protected _toggleOptionSet(
ev: { target: EditorOptionSetTarget | null },
options: EditorOptions,
): void {
protected _toggleMenu(ev: { target: EditorMenuTarget | null }): void {
if (ev && ev.target) {
const show = !options[ev.target.optionSetName].show;
for (const [key] of Object.entries(options)) {
options[key].show = false;
const domain = ev.target.domain;
const key = ev.target.key;
if (this._expandedMenus[domain] == key) {
this._closeMenu(domain);
} else {
this._openMenu(domain, key);
}
options[ev.target.optionSetName].show = show;
this.requestUpdate();
}
}
+16 -4
View File
@@ -159,10 +159,22 @@
"menu": {
"buttons": {
"frigate": "Frigate menu / Default view",
"frigate_ui": "Frigate user Interface",
"live": "Live",
"clips": "Clips",
"snapshots": "Snapshots",
"image": "Image",
"download": "Download",
"frigate_ui": "Frigate user interface",
"fullscreen": "Fullscreen",
"download": "Download event media",
"cameras": "Select camera"
"timeline": "Timeline",
"icon": "Icon",
"priority": "Priority",
"alignment": "Button alignment",
"alignments": {
"matching": "Matching the menu alignment",
"opposing": "Opposing the menu alignment"
},
"enabled": "Button enabled"
},
"style": "Menu style",
"styles": {
@@ -236,7 +248,7 @@
"image_secondary": "Static image view options",
"dimensions": "Dimensions",
"dimensions_secondary": "Dimensions & shape options",
"show_button": "Show button",
"button": "Button",
"upgrade": "Upgrade",
"upgrade_available": "An automatic card configuration upgrade is available",
"delete": "Delete",
+8 -8
View File
@@ -40,35 +40,35 @@ div.upgrade span {
padding: 10px;
}
.camera-header {
.submenu-header {
display: flex;
margin-top: 4px;
cursor: pointer;
}
.camera-header * {
.submenu-header * {
flex-basis: auto;
// Only allow clicks on the outermost header.
// Only allow clicks on the header.
pointer-events: none;
}
.camera-header ha-icon {
.submenu-header ha-icon {
padding-right: 10px;
}
.camera-header .new-camera {
.submenu-header .new-camera {
font-style: italic;
color: var(--secondary-text-color, 'black');
}
.cameras {
.submenu {
margin: 5px 5px 10px 20px;
}
.cameras .controls {
.submenu .controls {
display: inline-block;
margin-left: auto;
margin-right: 0px;
margin-bottom: 5px;
}
.cameras .controls ha-icon-button.button {
.submenu .controls ha-icon-button.button {
--mdc-icon-button-size: 32px;
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
}
+83 -32
View File
@@ -11,44 +11,78 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
/***********************************
* Aligned divs: matching & opposing
***********************************/
div.matching,
div.opposing {
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: flex-start;
// Allow the divs to be resized.
min-width: 0px;
min-height: 0px;
}
div.matching {
justify-content: flex-start;
}
div.opposing {
justify-content: flex-end;
}
/********************
* Outside menu style
********************/
:host([data-style='outside']) {
width: 100%;
background: var(--secondary-background-color);
}
:host([data-mode='outside'][data-position='top']) {
:host([data-style='outside'][data-position='top']) {
border-top-left-radius: var(--ha-card-border-radius, 4px);
border-top-right-radius: var(--ha-card-border-radius, 4px);
}
:host([data-mode='outside'][data-position='below']) {
:host([data-style='outside']:not([data-position='top'])) {
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
}
:host([data-position='top']),
:host([data-position='left'][data-alignment='top']),
:host([data-position='right'][data-alignment='top']) {
/**************************************
* Positioning for absolute menu styles
**************************************/
:host(:not([data-style='outside'])[data-position='top']),
:host(:not([data-style='outside'])[data-position='left'][data-alignment='top']),
:host(:not([data-style='outside'])[data-position='right'][data-alignment='top']) {
top: 0px;
}
:host([data-position='bottom']),
:host([data-position='left'][data-alignment='bottom']),
:host([data-position='right'][data-alignment='bottom']) {
:host(:not([data-style='outside'])[data-position='bottom']),
:host(:not([data-style='outside'])[data-position='left'][data-alignment='bottom']),
:host(:not([data-style='outside'])[data-position='right'][data-alignment='bottom']) {
bottom: 0px;
}
:host([data-position='left']),
:host([data-position='top'][data-alignment='left']),
:host([data-position='bottom'][data-alignment='left']) {
:host(:not([data-style='outside'])[data-position='left']),
:host(:not([data-style='outside'])[data-position='top'][data-alignment='left']),
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='left']) {
left: 0px;
}
:host([data-position='right']),
:host([data-position='top'][data-alignment='right']),
:host([data-position='bottom'][data-alignment='right']) {
:host(:not([data-style='outside'])[data-position='right']),
:host(:not([data-style='outside'])[data-position='top'][data-alignment='right']),
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='right']) {
right: 0px;
}
:host([data-position='left']) {
/********************************************************
* Hack: Ensure host & div expand for column flex layouts
********************************************************/
:host(:not([data-style='outside'])[data-position='left']) {
// Awful hack: Flexbox column wrapping doesn't work properly in most major
// browsers -- the element boundary does not expand to cover the full wrapped
// content as it should. This results in the wrapped 'content' appearing
@@ -61,24 +95,51 @@
// - https://bugs.chromium.org/p/chromium/issues/detail?id=507397
writing-mode: vertical-lr;
}
:host([data-position='right']) {
:host(:not([data-style='outside'])[data-position='right']) {
// See "Awful hack" above.
writing-mode: vertical-rl;
}
:host(:not([data-style='outside'])[data-style='overlay'][data-position='left']) div > *,
:host(:not([data-style='outside'])[data-style='overlay'][data-position='right']) div > *,
:host(:not([data-style='outside'])[data-style='hover'][data-position='left']) div > *,
:host(:not([data-style='outside'])[data-style='hover'][data-position='right']) div > *,
:host(:not([data-style='outside'])[data-style='hidden'][data-position='left']) div > *,
:host(:not([data-style='outside'])[data-style='hidden'][data-position='right']) div > * {
// See "Awful hack" above. Note that this "cancelation" of writing mode only
// affects from beyond the divs, i.e. the writing mode hack applies to host
// and divs both.
writing-mode: horizontal-tb;
}
:host([data-position='left'][data-alignment='bottom']),
:host([data-position='right'][data-alignment='bottom']),
:host([data-position='top'][data-alignment='right']),
:host([data-position='bottom'][data-alignment='right']) {
/**********************
* "Reverse" alignments
**********************/
:host(:not([data-style='outside'])[data-position='left'][data-alignment='bottom']),
:host(:not([data-style='outside'])[data-position='right'][data-alignment='bottom']),
:host(:not([data-style='outside'])[data-position='top'][data-alignment='right']),
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='right']),
:host(:not([data-style='outside'])[data-position='left'][data-alignment='bottom']) div,
:host(:not([data-style='outside'])[data-position='right'][data-alignment='bottom']) div,
:host(:not([data-style='outside'])[data-position='top'][data-alignment='right']) div,
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='right']) div {
flex-direction: row-reverse;
}
:host([data-position='bottom']) {
/****************************
* Wrap upwards on the bottom
****************************/
:host(:not([data-style='outside'])[data-position='bottom']) div {
// If the menu has more content that allows, "wrap upwards" to keep the
// Frigate button in the same place.
flex-wrap: wrap-reverse;
}
/********************************************
* Positioning for absolute based menu styles
********************************************/
:host([data-style='overlay']),
:host([data-style='hover']),
:host([data-style='hidden']) {
@@ -111,13 +172,3 @@
overflow: visible;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
}
:host([data-style='overlay'][data-position='left']) > *,
:host([data-style='overlay'][data-position='right']) > *,
:host([data-style='hover'][data-position='left']) > *,
:host([data-style='hover'][data-position='right']) > *,
:host([data-style='hidden'][data-position='left']) > *,
:host([data-style='hidden'][data-position='right']) > * {
// See "Awful hack" above.
writing-mode: horizontal-tb;
}
+64 -38
View File
@@ -53,21 +53,13 @@ const FRIGATE_CARD_VIEWS = [
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
const FRIGATE_MENU_STYLES = [
'none',
'hidden',
'overlay',
'hover',
'outside',
] as const;
const FRIGATE_MENU_POSITIONS = [
'left',
'right',
'top',
'bottom',
] as const;
const FRIGATE_MENU_STYLES = ['none', 'hidden', 'overlay', 'hover', 'outside'] as const;
const FRIGATE_MENU_POSITIONS = ['left', 'right', 'top', 'bottom'] as const;
const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS;
export const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
export const FRIGATE_MENU_PRIORITY_MAX = 100;
const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number];
@@ -335,15 +327,29 @@ export type CameraConfig = z.infer<typeof cameraConfigSchema>;
/**
* Custom Element Types.
*/
const menuBaseSchema = z.object({
enabled: z.boolean().default(true).optional(),
priority: z
.number()
.min(0)
.max(FRIGATE_MENU_PRIORITY_MAX)
.default(FRIGATE_MENU_PRIORITY_DEFAULT)
.optional(),
alignment: z.enum(['matching', 'opposing']).default('matching').optional(),
icon: z.string().optional(),
});
export const menuIconSchema = iconSchema.extend({
export const menuIconSchema = menuBaseSchema.merge(iconSchema).extend({
type: z.literal('custom:frigate-card-menu-icon'),
});
export type MenuIcon = z.infer<typeof menuIconSchema>;
export const menuStateIconSchema = stateIconSchema.extend({
type: z.literal('custom:frigate-card-menu-state-icon'),
});
export const menuStateIconSchema = menuBaseSchema
.merge(stateIconSchema)
.extend({
type: z.literal('custom:frigate-card-menu-state-icon'),
})
.merge(menuBaseSchema);
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
const menuSubmenuItemSchema = elementsBaseSchema.extend({
@@ -354,11 +360,12 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
});
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
export const menuSubmenuSchema = iconSchema.extend({
export const menuSubmenuSchema = menuBaseSchema.merge(iconSchema).extend({
type: z.literal('custom:frigate-card-menu-submenu'),
items: menuSubmenuItemSchema.array(),
});
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu;
const frigateCardConditionSchema = z.object({
view: z.string().array().optional(),
@@ -612,25 +619,44 @@ export type LiveConfig = z.infer<typeof liveConfigSchema>;
/**
* Menu configuration section.
*/
const visibleButtonDefault = {
priority: FRIGATE_MENU_PRIORITY_DEFAULT,
enabled: true,
};
const hiddenButtonDefault = {
priority: FRIGATE_MENU_PRIORITY_DEFAULT,
enabled: false,
};
const menuConfigDefault = {
style: 'hidden' as const,
position: 'top' as const,
alignment: 'left' as const,
buttons: {
frigate: true,
cameras: true,
live: true,
clips: true,
snapshots: true,
image: false,
timeline: true,
download: true,
frigate_ui: true,
fullscreen: true,
frigate: visibleButtonDefault,
cameras: visibleButtonDefault,
live: visibleButtonDefault,
clips: visibleButtonDefault,
snapshots: visibleButtonDefault,
image: hiddenButtonDefault,
timeline: visibleButtonDefault,
download: visibleButtonDefault,
frigate_ui: visibleButtonDefault,
fullscreen: visibleButtonDefault,
},
button_size: 40,
};
const visibleButtonSchema = menuBaseSchema.extend({
enabled: menuBaseSchema.shape.enabled.default(visibleButtonDefault.enabled),
priority: menuBaseSchema.shape.priority.default(visibleButtonDefault.priority),
});
const hiddenButtonSchema = menuBaseSchema.extend({
enabled: menuBaseSchema.shape.enabled.default(hiddenButtonDefault.enabled),
priority: menuBaseSchema.shape.priority.default(hiddenButtonDefault.priority),
});
const menuConfigSchema = z
.object({
style: z.enum(FRIGATE_MENU_STYLES).default(menuConfigDefault.style),
@@ -638,16 +664,16 @@ const menuConfigSchema = z
alignment: z.enum(FRIGATE_MENU_ALIGNMENTS).default(menuConfigDefault.alignment),
buttons: z
.object({
frigate: z.boolean().default(menuConfigDefault.buttons.frigate),
cameras: z.boolean().default(menuConfigDefault.buttons.cameras),
live: z.boolean().default(menuConfigDefault.buttons.live),
clips: z.boolean().default(menuConfigDefault.buttons.clips),
snapshots: z.boolean().default(menuConfigDefault.buttons.snapshots),
image: z.boolean().default(menuConfigDefault.buttons.image),
timeline: z.boolean().default(menuConfigDefault.buttons.timeline),
download: z.boolean().default(menuConfigDefault.buttons.download),
frigate_ui: z.boolean().default(menuConfigDefault.buttons.frigate_ui),
fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen),
frigate: visibleButtonSchema.default(menuConfigDefault.buttons.frigate),
cameras: visibleButtonSchema.default(menuConfigDefault.buttons.cameras),
live: visibleButtonSchema.default(menuConfigDefault.buttons.live),
clips: visibleButtonSchema.default(menuConfigDefault.buttons.clips),
snapshots: visibleButtonSchema.default(menuConfigDefault.buttons.snapshots),
image: hiddenButtonSchema.default(menuConfigDefault.buttons.image),
timeline: visibleButtonSchema.default(menuConfigDefault.buttons.timeline),
download: visibleButtonSchema.default(menuConfigDefault.buttons.download),
frigate_ui: visibleButtonSchema.default(menuConfigDefault.buttons.frigate_ui),
fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen),
})
.default(menuConfigDefault.buttons),
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),