Initial skeleton menu button priority/alignment.
This commit is contained in:
+15
-1
@@ -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
@@ -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>`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
+83
-39
@@ -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({
|
||||
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({
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user