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),