diff --git a/src/card.ts b/src/card.ts
index 1b57c24d..56e0dbc1 100644
--- a/src/card.ts
+++ b/src/card.ts
@@ -1203,6 +1203,9 @@ export class FrigateCard extends LitElement {
};
const actions = this._getMergedActions();
+ const renderMenuAbove =
+ this._getConfig().menu.style === 'outside' &&
+ this._getConfig().menu.position === 'top';
return html` this.requestUpdate()}
>
- ${this._getConfig().menu.mode == 'above' ? this._renderMenu() : ''}
+ ${renderMenuAbove ? this._renderMenu() : ''}
${this._cameras === undefined
@@ -1261,7 +1264,7 @@ export class FrigateCard extends LitElement {
}
- ${this._getConfig().menu.mode != 'above' ? this._renderMenu() : ''}
+ ${!renderMenuAbove ? this._renderMenu() : ''}
`;
}
diff --git a/src/components/menu.ts b/src/components/menu.ts
index 37964c27..98b4a8d5 100644
--- a/src/components/menu.ts
+++ b/src/components/menu.ts
@@ -64,8 +64,11 @@ export class FrigateCardMenu extends LitElement {
`${menuConfig.button_size}px`,
);
}
- // Store the menu mode as an attribute (used for CSS attribute selectors).
- this.setAttribute('data-mode', menuConfig.mode);
+ // Store the menu style, position and alignment as attributes (used for
+ // styling).
+ this.setAttribute('data-style', menuConfig.style);
+ this.setAttribute('data-position', menuConfig.position);
+ this.setAttribute('data-alignment', menuConfig.alignment);
}
@state()
protected _menuConfig?: MenuConfig;
@@ -82,7 +85,7 @@ export class FrigateCardMenu extends LitElement {
* @returns `true` if the menu is hiding, `false` otherwise.
*/
static isHidingMenu(menuConfig: MenuConfig | undefined): boolean {
- return menuConfig?.mode.startsWith('hidden-') ?? false;
+ return menuConfig?.style === 'hidden' ?? false;
}
/**
@@ -265,15 +268,14 @@ export class FrigateCardMenu extends LitElement {
if (!this._menuConfig) {
return;
}
- const mode = this._menuConfig.mode;
-
- if (mode == 'none') {
+ const style = this._menuConfig.style;
+ if (style === 'none') {
return;
}
// If the hidden menu isn't expanded, only show the Frigate button.
const buttons =
- !mode.startsWith('hidden-') || this.expanded
+ style !== 'hidden' || this.expanded
? this.buttons
: this.buttons.filter((button) => button.icon === FRIGATE_BUTTON_MENU_ICON);
return html` ${buttons.map((button) => this._renderButton(button))} `;
diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts
index fd8c69d1..c240fc3e 100644
--- a/src/config-mgmt.ts
+++ b/src/config-mgmt.ts
@@ -19,7 +19,6 @@ import {
CONF_LIVE_WEBRTC_CARD,
CONF_MENU,
CONF_MENU_BUTTON_SIZE,
- CONF_MENU_MODE,
CONF_OVERRIDES,
CONF_VIEW_DEFAULT,
CONF_VIEW_TIMEOUT_SECONDS,
@@ -415,7 +414,7 @@ const UPGRADES = [
upgradeMoveTo('autoplay_clip', 'event_viewer.autoplay_clip'),
upgradeMoveTo('controls.nextprev', CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE),
upgradeMoveTo('controls.nextprev_size', CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE),
- upgradeMoveTo('menu_mode', CONF_MENU_MODE),
+ upgradeMoveTo('menu_mode', "menu.mode"),
upgradeMoveTo('menu_buttons', 'menu.buttons'),
upgradeMoveTo('menu_button_size', CONF_MENU_BUTTON_SIZE),
upgradeMoveTo('image', 'image.src', isNotObject),
diff --git a/src/const.ts b/src/const.ts
index 6e18ebfe..11ea8e81 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -108,6 +108,9 @@ export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
`${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_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 =
@@ -119,7 +122,6 @@ 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_IMAGE = `${CONF_MENU}.buttons.image` as const;
export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const;
-export const CONF_MENU_MODE = `${CONF_MENU}.mode` 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 9e66ae70..6fd12978 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -64,6 +64,7 @@ import {
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_PRELOAD,
CONF_LIVE_TRANSITION_EFFECT,
+ CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTONS_CLIPS,
CONF_MENU_BUTTONS_FRIGATE,
CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD,
@@ -73,7 +74,8 @@ import {
CONF_MENU_BUTTONS_LIVE,
CONF_MENU_BUTTONS_SNAPSHOTS,
CONF_MENU_BUTTON_SIZE,
- CONF_MENU_MODE,
+ CONF_MENU_POSITION,
+ CONF_MENU_STYLE,
CONF_TIMELINE_CLUSTERING_THRESHOLD,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
@@ -89,11 +91,7 @@ import {
CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS,
} from './const.js';
-import {
- arrayMove,
- getCameraID,
- getCameraTitle,
-} from './common.js';
+import { arrayMove, getCameraID, getCameraTitle } from './common.js';
import {
copyConfig,
deleteConfigValue,
@@ -219,23 +217,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'current', label: localize('config.view.views.current') },
];
- protected _menuModes: EditorSelectOption[] = [
+ protected _menuStyles: EditorSelectOption[] = [
{ value: '', label: '' },
- { value: 'none', label: localize('config.menu.modes.none') },
- { value: 'hidden-top', label: localize('config.menu.modes.hidden-top') },
- { value: 'hidden-left', label: localize('config.menu.modes.hidden-left') },
- { value: 'hidden-bottom', label: localize('config.menu.modes.hidden-bottom') },
- { value: 'hidden-right', label: localize('config.menu.modes.hidden-right') },
- { value: 'overlay-top', label: localize('config.menu.modes.overlay-top') },
- { value: 'overlay-left', label: localize('config.menu.modes.overlay-left') },
- { value: 'overlay-bottom', label: localize('config.menu.modes.overlay-bottom') },
- { value: 'overlay-right', label: localize('config.menu.modes.overlay-right') },
- { value: 'hover-top', label: localize('config.menu.modes.hover-top') },
- { value: 'hover-left', label: localize('config.menu.modes.hover-left') },
- { value: 'hover-bottom', label: localize('config.menu.modes.hover-bottom') },
- { value: 'hover-right', label: localize('config.menu.modes.hover-right') },
- { value: 'above', label: localize('config.menu.modes.above') },
- { value: 'below', label: localize('config.menu.modes.below') },
+ { value: 'none', label: localize('config.menu.styles.none') },
+ { value: 'hidden', label: localize('config.menu.styles.hidden') },
+ { value: 'overlay', label: localize('config.menu.styles.overlay') },
+ { value: 'hover', label: localize('config.menu.styles.hover') },
+ { value: 'outside', label: localize('config.menu.styles.outside') },
+ ];
+
+ protected _menuPositions: EditorSelectOption[] = [
+ { value: '', label: '' },
+ { value: 'left', label: localize('config.menu.positions.left') },
+ { value: 'right', label: localize('config.menu.positions.right') },
+ { value: 'top', label: localize('config.menu.positions.top') },
+ { value: 'bottom', label: localize('config.menu.positions.bottom') },
+ ];
+
+ protected _menuAlignments: EditorSelectOption[] = [
+ { value: '', label: '' },
+ { value: 'left', label: localize('config.menu.alignments.left') },
+ { value: 'right', label: localize('config.menu.alignments.right') },
+ { value: 'top', label: localize('config.menu.alignments.top') },
+ { value: 'bottom', label: localize('config.menu.alignments.bottom') },
];
protected _eventViewerNextPreviousControlStyles: EditorSelectOption[] = [
@@ -878,7 +882,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.menu.show
? html`
- ${this._renderOptionSelector(CONF_MENU_MODE, this._menuModes)}
+ ${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,
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index 662f6694..71cd6167 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -164,23 +164,27 @@
"download": "Download event media",
"cameras": "Select camera"
},
- "mode": "Menu mode",
- "modes": {
+ "style": "Menu style",
+ "styles": {
"none": "No menu",
- "hidden-top": "Hidden top",
- "hidden-left": "Hidden left",
- "hidden-bottom": "Hidden bottom",
- "hidden-right": "Hidden right",
- "overlay-top": "Overlay top",
- "overlay-left": "Overlay left",
- "overlay-bottom": "Overlay bottom",
- "overlay-right": "Overlay right",
- "hover-top": "Hover top",
- "hover-left": "Hover left",
- "hover-bottom": "Hover bottom",
- "hover-right": "Hover right",
- "above": "Above",
- "below": "Below"
+ "hidden": "Hidden menu",
+ "overlay": "Overlay menu",
+ "hover": "Hover menu",
+ "outside": "Outside menu"
+ },
+ "position": "Menu position",
+ "positions": {
+ "left": "Positioned on the left",
+ "right": "Positioned on the right",
+ "top": "Positioned on the top",
+ "bottom": "Positioned on the bottom"
+ },
+ "alignment": "Menu alignment",
+ "alignments": {
+ "left": "Aligned to the left",
+ "right": "Aligned to the right",
+ "top": "Aligned to the top",
+ "bottom": "Aligned to the bottom"
},
"button_size": "Menu button size in pixels"
},
diff --git a/src/scss/card.scss b/src/scss/card.scss
index f74a82a0..aa293986 100644
--- a/src/scss/card.scss
+++ b/src/scss/card.scss
@@ -29,11 +29,14 @@
height: inherit;
margin: auto;
overflow: auto;
- -ms-overflow-style: none; /* Hide scrollbar: IE and Edge */
- scrollbar-width: none; /* Hide scrollbar: Firefox */
display: flex;
align-items: center;
justify-content: center;
+
+ // Hide scrollbar: Firefox
+ scrollbar-width: none;
+ // Hide scrollbar: IE and Edge
+ -ms-overflow-style: none;
}
/* Hide scrollbar for Chrome, Safari and Opera */
@@ -51,16 +54,17 @@
}
/* The 'hover' menu mode is styling applied outside of the menu itself */
-frigate-card-menu[data-mode^=hover-] {
+frigate-card-menu[data-style='hover'] {
z-index: 1;
transition: opacity 0.5s ease;
}
-.outer + frigate-card-menu[data-mode^=hover-] {
- opacity: 0.0;
+.outer + frigate-card-menu[data-style='hover'] {
+ opacity: 0;
}
-.outer:hover + frigate-card-menu[data-mode^=hover-], frigate-card-menu[data-mode^=hover-]:hover {
- opacity: 1.0;
+.outer:hover + frigate-card-menu[data-style='hover'],
+frigate-card-menu[data-mode='hover']:hover {
+ opacity: 1;
}
ha-card {
@@ -78,7 +82,7 @@ ha-card {
border-radius: var(--ha-card-border-radius, 4px);
}
-frigate-card-live.hidden {
+frigate-card-live.hidden {
// Live view will be rendered but hidden for live preloading.
display: none;
}
diff --git a/src/scss/menu.scss b/src/scss/menu.scss
index 45063aa3..af0fadda 100644
--- a/src/scss/menu.scss
+++ b/src/scss/menu.scss
@@ -14,23 +14,37 @@
flex-wrap: wrap;
}
-:host([data-mode="above"]),:host([data-mode="below"]) {
+:host([data-style='outside']) {
width: 100%;
background: var(--secondary-background-color);
}
-:host([data-mode="above"]) {
+:host([data-mode='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="below"]) {
+:host([data-mode='outside'][data-position='below']) {
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
}
-:host([data-mode$="-left"]) {
- left: 0px;
+:host([data-position='top']),
+:host([data-alignment='top']) {
top: 0px;
+}
+:host([data-position='bottom']),
+:host([data-alignment='bottom']) {
+ bottom: 0px;
+}
+:host([data-position='left']),
+:host([data-alignment='left']) {
+ left: 0px;
+}
+:host([data-position='right']),
+:host([data-alignment='right']) {
+ right: 0px;
+}
+:host([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
@@ -43,66 +57,63 @@
// - https://bugs.chromium.org/p/chromium/issues/detail?id=507397
writing-mode: vertical-lr;
}
-
-:host([data-mode$="-right"]) {
- right: 0px;
- top: 0px;
-
+:host([data-position='right']) {
// See "Awful hack" above.
writing-mode: vertical-rl;
}
-:host([data-mode$="-top"]) {
- left: 0px;
- top: 0px;
+: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']) {
+ flex-direction: row-reverse;
}
-:host([data-mode$="-bottom"]) {
- left: 0px;
- bottom: 0px;
-
+:host([data-position='bottom']) {
// If the menu has more content that allows, "wrap upwards" to keep the
// Frigate button in the same place.
flex-wrap: wrap-reverse;
}
-:host([data-mode^="overlay-"]),:host([data-mode^="hover-"]),:host([data-mode^="hidden-"]) {
+:host([data-style='overlay']),
+:host([data-style='hover']),
+:host([data-style='hidden']) {
position: absolute;
overflow: hidden;
width: calc(var(--frigate-card-menu-button-size) + 6px);
height: calc(var(--frigate-card-menu-button-size) + 6px);
}
-:host([data-mode="overlay-top"]),
-:host([data-mode="overlay-bottom"]),
-:host([data-mode="hover-top"]),
-:host([data-mode="hover-bottom"]),
-:host([data-mode="hidden-top"][expanded]),
-:host([data-mode="hidden-bottom"][expanded]) {
+:host([data-style='overlay'][data-position='top']),
+:host([data-style='overlay'][data-position='bottom']),
+:host([data-style='hover'][data-position='top']),
+:host([data-style='hover'][data-position='bottom']),
+:host([data-style='hidden'][data-position='top'][expanded]),
+:host([data-style='hidden'][data-position='bottom'][expanded]) {
width: 100%;
height: auto;
overflow: visible;
- background: linear-gradient(90deg, rgba(0,0,0,0.3), rgba(0,0,0,0));
+ background: linear-gradient(90deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
}
-:host([data-mode="overlay-left"]),
-:host([data-mode="overlay-right"]),
-:host([data-mode="hover-left"]),
-:host([data-mode="hover-right"]),
-:host([data-mode="hidden-left"][expanded]),
-:host([data-mode="hidden-right"][expanded]) {
+: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'][expanded]),
+:host([data-style='hidden'][data-position='right'][expanded]) {
height: 100%;
width: auto;
overflow: visible;
- background: linear-gradient(180deg, rgba(0,0,0,0.3), rgba(0,0,0,0));
+ background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
}
-:host([data-mode="overlay-left"]) > *,
-:host([data-mode="hover-left"]) > *,
-:host([data-mode="hidden-left"]) > *,
-:host([data-mode="overlay-right"]) > *,
-:host([data-mode="hover-right"]) > *,
-:host([data-mode="hidden-right"]) > * {
+: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 00cd0af5..17e0aa35 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -53,23 +53,21 @@ const FRIGATE_CARD_VIEWS = [
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
-const FRIGATE_MENU_MODES = [
+const FRIGATE_MENU_STYLES = [
'none',
- 'hidden-top',
- 'hidden-left',
- 'hidden-bottom',
- 'hidden-right',
- 'overlay-top',
- 'overlay-left',
- 'overlay-bottom',
- 'overlay-right',
- 'hover-top',
- 'hover-left',
- 'hover-bottom',
- 'hover-right',
- 'above',
- 'below',
+ 'hidden',
+ 'overlay',
+ 'hover',
+ 'outside',
] as const;
+const FRIGATE_MENU_POSITIONS = [
+ 'left',
+ 'right',
+ 'top',
+ 'bottom',
+] as const;
+const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS;
+
const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number];
@@ -615,7 +613,9 @@ export type LiveConfig = z.infer;
* Menu configuration section.
*/
const menuConfigDefault = {
- mode: 'hidden-top' as const,
+ style: 'hidden' as const,
+ position: 'top' as const,
+ alignment: 'left' as const,
buttons: {
frigate: true,
cameras: true,
@@ -633,7 +633,9 @@ const menuConfigDefault = {
const menuConfigSchema = z
.object({
- mode: z.enum(FRIGATE_MENU_MODES).default(menuConfigDefault.mode),
+ style: z.enum(FRIGATE_MENU_STYLES).default(menuConfigDefault.style),
+ position: z.enum(FRIGATE_MENU_POSITIONS).default(menuConfigDefault.position),
+ alignment: z.enum(FRIGATE_MENU_ALIGNMENTS).default(menuConfigDefault.alignment),
buttons: z
.object({
frigate: z.boolean().default(menuConfigDefault.buttons.frigate),