diff --git a/README.md b/README.md index d2f1b06a..fe8649f3 100644 --- a/README.md +++ b/README.md @@ -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: ``` +### Overriding default menu behavior + +
+ Expand: Overriding default menu behavior + +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 +``` +
+ ### 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). diff --git a/src/card.ts b/src/card.ts index 56e0dbc1..00f26fda 100644 --- a/src/card.ts +++ b/src/card.ts @@ -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) { diff --git a/src/components/menu.ts b/src/components/menu.ts index 98b4a8d5..3336289b 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -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`