Initial skeleton menu button priority/alignment.

This commit is contained in:
Dermot Duffy
2022-04-26 20:20:32 -07:00
parent 241ee4b204
commit ccd47d9a9d
7 changed files with 359 additions and 127 deletions
+15 -1
View File
@@ -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) {
+87 -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,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` <div class="near" style="${styleMap(nearStyle)}">
${nearButtons.map((button) => this._renderButton(button))}
</div>
<div class="far" style="${styleMap(farStyle)}">
${farButtons.map((button) => this._renderButton(button))}
</div>`;
}
/**
+35
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,
@@ -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),
];
+25 -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,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;
+29 -28
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,
@@ -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,
)}
</div>
`
+82 -32
View File
@@ -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;
}
+86 -42
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];
@@ -336,14 +328,23 @@ export type CameraConfig = z.infer<typeof cameraConfigSchema>;
* 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<typeof menuIconSchema>;
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<typeof menuStateIconSchema>;
const menuSubmenuItemSchema = elementsBaseSchema.extend({
@@ -354,11 +355,14 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
});
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
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<typeof menuSubmenuSchema>;
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),