diff --git a/src/card.ts b/src/card.ts
index 56e0dbc1..e742d60b 100644
--- a/src/card.ts
+++ b/src/card.ts
@@ -285,6 +285,7 @@ export class FrigateCard extends LitElement {
// 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).
+ priority: this._getConfig().menu.buttons.frigate,
icon: FRIGATE_BUTTON_MENU_ICON,
tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu)
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
@@ -311,6 +312,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-submenu',
title: localize('config.menu.buttons.cameras'),
+ priority: this._getConfig().menu.buttons.cameras,
icon: 'mdi:video-switch',
items: menuItems,
});
@@ -320,6 +322,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.live'),
+ priority: this._getConfig().menu.buttons.live,
icon: 'mdi:cctv',
style: this._view?.is('live') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
@@ -339,6 +342,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.clips'),
+ priority: this._getConfig().menu.buttons.clips,
icon: 'mdi:filmstrip',
style: this._view?.is('clips') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
@@ -357,6 +361,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.snapshots'),
+ priority: this._getConfig().menu.buttons.snapshots,
icon: 'mdi:camera',
style: this._view?.is('snapshots') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction(
@@ -372,6 +377,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.image'),
+ priority: this._getConfig().menu.buttons.image,
icon: 'mdi:image',
style: this._view?.is('image') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
@@ -382,6 +388,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.timeline'),
+ priority: this._getConfig().menu.buttons.timeline,
icon: 'mdi:chart-gantt',
style: this._view?.is('timeline') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction,
@@ -395,6 +402,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.download'),
+ priority: this._getConfig().menu.buttons.download,
icon: 'mdi:download',
tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
});
@@ -404,6 +412,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.frigate_ui'),
+ priority: this._getConfig().menu.buttons.frigate_ui,
icon: 'mdi:web',
tap_action: createFrigateCardCustomAction(
'frigate_ui',
@@ -415,6 +424,7 @@ export class FrigateCard extends LitElement {
buttons.push({
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.fullscreen'),
+ priority: this._getConfig().menu.buttons.fullscreen,
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
tap_action: createFrigateCardCustomAction(
'fullscreen',
@@ -489,7 +499,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..e611a3b3 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,6 +206,61 @@ 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;
+ }
+ }
+
+ // TODO below might be removable?
+
+ // If the alignments are different, sort 'opposite' alignments later.
+ if (a.alignment !== b.alignment) {
+ if (
+ (a.alignment === undefined || a.alignment === 'near') &&
+ b.alignment === 'far'
+ ) {
+ return -1;
+ }
+ if (
+ (b.alignment === undefined || b.alignment === 'near') &&
+ a.alignment === 'far'
+ ) {
+ 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.
@@ -274,11 +337,31 @@ export class FrigateCardMenu extends LitElement {
}
// If the hidden menu isn't expanded, only show the Frigate button.
- const buttons =
+ const nearButtons =
style !== 'hidden' || this.expanded
- ? this.buttons
+ ? this.buttons.filter(
+ (button) => !button.alignment || button.alignment === 'near',
+ )
: this.buttons.filter((button) => button.icon === FRIGATE_BUTTON_MENU_ICON);
- return html` ${buttons.map((button) => this._renderButton(button))} `;
+
+ const farButtons =
+ style !== 'hidden' || this.expanded
+ ? this.buttons.filter((button) => button.alignment === 'far')
+ : [];
+
+ const nearStyle = {
+ flex: String(nearButtons.length),
+ };
+ const farStyle = {
+ flex: String(farButtons.length),
+ };
+
+ return html`
+ ${nearButtons.map((button) => this._renderButton(button))}
+
+
+ ${farButtons.map((button) => this._renderButton(button))}
+
`;
}
/**
diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts
index e476d7c0..ac681a7d 100644
--- a/src/config-mgmt.ts
+++ b/src/config-mgmt.ts
@@ -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,
@@ -28,6 +37,7 @@ import {
} from './const';
import {
BUTTON_SIZE_MIN,
+ FRIGATE_MENU_PRIORITY_DEFAULT,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX,
@@ -476,6 +486,21 @@ 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 menuButtonToPriority = function (value: unknown): number | null | undefined {
+ if (typeof value === 'number') {
+ return undefined;
+ }
+ if (typeof value !== 'boolean') {
+ return null;
+ }
+ return value ? FRIGATE_MENU_PRIORITY_DEFAULT : 0;
+};
+
const UPGRADES = [
// v1.2.1 -> v2.0.0
upgradeMoveTo('frigate_url', 'frigate.url'),
@@ -539,4 +564,14 @@ const UPGRADES = [
),
upgradeWithOverrides('event_gallery.min_columns', deleteProperty),
upgradeMenuModeToStyleAndPosition(),
+
+ upgradeWithOverrides(CONF_MENU_BUTTONS_FRIGATE, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_CAMERAS, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_LIVE, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_CLIPS, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_SNAPSHOTS, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_IMAGE, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_DOWNLOAD, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_FRIGATE_UI, menuButtonToPriority),
+ upgradeWithOverrides(CONF_MENU_BUTTONS_FULLSCREEN, menuButtonToPriority),
];
diff --git a/src/const.ts b/src/const.ts
index 11ea8e81..9dc952a3 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -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,33 @@ 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_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;
diff --git a/src/editor.ts b/src/editor.ts
index 6fd12978..4ada56eb 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -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,
@@ -67,9 +68,9 @@ import {
CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTONS_CLIPS,
CONF_MENU_BUTTONS_FRIGATE,
- CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD,
- CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN,
+ CONF_MENU_BUTTONS_DOWNLOAD,
CONF_MENU_BUTTONS_FRIGATE_UI,
+ CONF_MENU_BUTTONS_FULLSCREEN,
CONF_MENU_BUTTONS_IMAGE,
CONF_MENU_BUTTONS_LIVE,
CONF_MENU_BUTTONS_SNAPSHOTS,
@@ -886,45 +887,45 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${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(
+ ${this._renderNumberInput(
CONF_MENU_BUTTONS_FRIGATE,
- defaults.menu.buttons.frigate,
- getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE),
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
- ${this._renderSwitch(
+ ${this._renderNumberInput(
CONF_MENU_BUTTONS_LIVE,
- defaults.menu.buttons.live,
- getShowButtonLabel('view.views.live'),
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
- ${this._renderSwitch(
+ ${this._renderNumberInput(
CONF_MENU_BUTTONS_CLIPS,
- defaults.menu.buttons.clips,
- getShowButtonLabel('view.views.clips'),
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
- ${this._renderSwitch(
+ ${this._renderNumberInput(
CONF_MENU_BUTTONS_SNAPSHOTS,
- defaults.menu.buttons.snapshots,
- getShowButtonLabel('view.views.snapshots'),
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
- ${this._renderSwitch(
+ ${this._renderNumberInput(
CONF_MENU_BUTTONS_IMAGE,
- defaults.menu.buttons.image,
- getShowButtonLabel('view.views.image'),
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
- ${this._renderSwitch(
- CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD,
- defaults.menu.buttons.download,
- getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD),
+ ${this._renderNumberInput(
+ CONF_MENU_BUTTONS_DOWNLOAD,
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
- ${this._renderSwitch(
+ ${this._renderNumberInput(
CONF_MENU_BUTTONS_FRIGATE_UI,
- defaults.menu.buttons.frigate_ui,
- getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_UI),
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
- ${this._renderSwitch(
- CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN,
- defaults.menu.buttons.fullscreen,
- getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN),
+ ${this._renderNumberInput(
+ CONF_MENU_BUTTONS_FULLSCREEN,
+ 0,
+ FRIGATE_MENU_PRIORITY_MAX,
)}
`
diff --git a/src/scss/menu.scss b/src/scss/menu.scss
index 73c5fd09..4a05520f 100644
--- a/src/scss/menu.scss
+++ b/src/scss/menu.scss
@@ -11,44 +11,77 @@
display: flex;
flex-direction: row;
- flex-wrap: wrap;
+ justify-content: space-between;
}
+/**************************
+ * Aligned divs: near & far
+ **************************/
+
+div.near,
+div.far {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+
+ // Allow the divs to be resized.
+ min-width: 0px;
+ min-height: 0px;
+}
+div.near {
+ justify-content: flex-start;
+}
+div.far {
+ 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 +94,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 +171,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;
-}
diff --git a/src/types.ts b/src/types.ts
index 17e0aa35..eb3b8046 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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];
@@ -336,14 +328,23 @@ export type CameraConfig = z.infer;
* Custom Element Types.
*/
-export const menuIconSchema = iconSchema.extend({
- type: z.literal('custom:frigate-card-menu-icon'),
+const menuBaseSchema = z.object({
+ priority: z.number().optional(),
+ alignment: z.enum(["near", "far"]).optional(),
});
+
+export const menuIconSchema = iconSchema
+ .extend({
+ type: z.literal('custom:frigate-card-menu-icon'),
+ })
+ .merge(menuBaseSchema);
export type MenuIcon = z.infer;
-export const menuStateIconSchema = stateIconSchema.extend({
- type: z.literal('custom:frigate-card-menu-state-icon'),
-});
+export const menuStateIconSchema = stateIconSchema
+ .extend({
+ type: z.literal('custom:frigate-card-menu-state-icon'),
+ })
+ .merge(menuBaseSchema);
export type MenuStateIcon = z.infer;
const menuSubmenuItemSchema = elementsBaseSchema.extend({
@@ -354,11 +355,14 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
});
export type MenuSubmenuItem = z.infer;
-export const menuSubmenuSchema = iconSchema.extend({
- type: z.literal('custom:frigate-card-menu-submenu'),
- items: menuSubmenuItemSchema.array(),
-});
+export const menuSubmenuSchema = iconSchema
+ .extend({
+ type: z.literal('custom:frigate-card-menu-submenu'),
+ items: menuSubmenuItemSchema.array(),
+ })
+ .merge(menuBaseSchema);
export type MenuSubmenu = z.infer;
+export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu;
const frigateCardConditionSchema = z.object({
view: z.string().array().optional(),
@@ -617,16 +621,16 @@ const menuConfigDefault = {
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: FRIGATE_MENU_PRIORITY_DEFAULT,
+ cameras: FRIGATE_MENU_PRIORITY_DEFAULT,
+ live: FRIGATE_MENU_PRIORITY_DEFAULT,
+ clips: FRIGATE_MENU_PRIORITY_DEFAULT,
+ snapshots: FRIGATE_MENU_PRIORITY_DEFAULT,
+ image: 0,
+ timeline: FRIGATE_MENU_PRIORITY_DEFAULT,
+ download: FRIGATE_MENU_PRIORITY_DEFAULT,
+ frigate_ui: FRIGATE_MENU_PRIORITY_DEFAULT,
+ fullscreen: FRIGATE_MENU_PRIORITY_DEFAULT,
},
button_size: 40,
};
@@ -638,16 +642,56 @@ 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: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.frigate),
+ cameras: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.cameras),
+ live: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.live),
+ clips: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.clips),
+ snapshots: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.snapshots),
+ image: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.image),
+ timeline: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.timeline),
+ download: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.download),
+ frigate_ui: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.frigate_ui),
+ fullscreen: z
+ .number()
+ .min(0)
+ .max(FRIGATE_MENU_PRIORITY_MAX)
+ .default(menuConfigDefault.buttons.fullscreen),
})
.default(menuConfigDefault.buttons),
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),