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 // Use a magic icon value that the menu will use to render the icon as
// it deems appropriate (certain menu configurations change the menu // it deems appropriate (certain menu configurations change the menu
// icon for the 'Frigate' button). // icon for the 'Frigate' button).
priority: this._getConfig().menu.buttons.frigate,
icon: FRIGATE_BUTTON_MENU_ICON, icon: FRIGATE_BUTTON_MENU_ICON,
tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu) tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu)
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction) ? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
@@ -311,6 +312,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-submenu', type: 'custom:frigate-card-menu-submenu',
title: localize('config.menu.buttons.cameras'), title: localize('config.menu.buttons.cameras'),
priority: this._getConfig().menu.buttons.cameras,
icon: 'mdi:video-switch', icon: 'mdi:video-switch',
items: menuItems, items: menuItems,
}); });
@@ -320,6 +322,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.live'), title: localize('config.view.views.live'),
priority: this._getConfig().menu.buttons.live,
icon: 'mdi:cctv', icon: 'mdi:cctv',
style: this._view?.is('live') ? this._getEmphasizedStyle() : {}, style: this._view?.is('live') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction, tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
@@ -339,6 +342,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.clips'), title: localize('config.view.views.clips'),
priority: this._getConfig().menu.buttons.clips,
icon: 'mdi:filmstrip', icon: 'mdi:filmstrip',
style: this._view?.is('clips') ? this._getEmphasizedStyle() : {}, style: this._view?.is('clips') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction, tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
@@ -357,6 +361,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.snapshots'), title: localize('config.view.views.snapshots'),
priority: this._getConfig().menu.buttons.snapshots,
icon: 'mdi:camera', icon: 'mdi:camera',
style: this._view?.is('snapshots') ? this._getEmphasizedStyle() : {}, style: this._view?.is('snapshots') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction( tap_action: createFrigateCardCustomAction(
@@ -372,6 +377,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.image'), title: localize('config.view.views.image'),
priority: this._getConfig().menu.buttons.image,
icon: 'mdi:image', icon: 'mdi:image',
style: this._view?.is('image') ? this._getEmphasizedStyle() : {}, style: this._view?.is('image') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction, tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
@@ -382,6 +388,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.timeline'), title: localize('config.view.views.timeline'),
priority: this._getConfig().menu.buttons.timeline,
icon: 'mdi:chart-gantt', icon: 'mdi:chart-gantt',
style: this._view?.is('timeline') ? this._getEmphasizedStyle() : {}, style: this._view?.is('timeline') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction, tap_action: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction,
@@ -395,6 +402,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.download'), title: localize('config.menu.buttons.download'),
priority: this._getConfig().menu.buttons.download,
icon: 'mdi:download', icon: 'mdi:download',
tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction, tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
}); });
@@ -404,6 +412,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.frigate_ui'), title: localize('config.menu.buttons.frigate_ui'),
priority: this._getConfig().menu.buttons.frigate_ui,
icon: 'mdi:web', icon: 'mdi:web',
tap_action: createFrigateCardCustomAction( tap_action: createFrigateCardCustomAction(
'frigate_ui', 'frigate_ui',
@@ -415,6 +424,7 @@ export class FrigateCard extends LitElement {
buttons.push({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.fullscreen'), title: localize('config.menu.buttons.fullscreen'),
priority: this._getConfig().menu.buttons.fullscreen,
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen', icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
tap_action: createFrigateCardCustomAction( tap_action: createFrigateCardCustomAction(
'fullscreen', 'fullscreen',
@@ -489,7 +499,11 @@ export class FrigateCard extends LitElement {
}; };
if (this._getConfig().cameras && Array.isArray(this._getConfig().cameras)) { 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) { 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 { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
@@ -14,6 +21,7 @@ import type {
ActionType, ActionType,
MenuButton, MenuButton,
MenuConfig, MenuConfig,
MenuItem,
StateParameters, StateParameters,
} from '../types.js'; } from '../types.js';
import { 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. * Render a button.
* @param button The button configuration to render. * @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. // If the hidden menu isn't expanded, only show the Frigate button.
const buttons = const nearButtons =
style !== 'hidden' || this.expanded 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); : 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_PRELOAD,
CONF_LIVE_WEBRTC_CARD, CONF_LIVE_WEBRTC_CARD,
CONF_MENU, 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_BUTTON_SIZE,
CONF_MENU_POSITION, CONF_MENU_POSITION,
CONF_MENU_STYLE, CONF_MENU_STYLE,
@@ -28,6 +37,7 @@ import {
} from './const'; } from './const';
import { import {
BUTTON_SIZE_MIN, BUTTON_SIZE_MIN,
FRIGATE_MENU_PRIORITY_DEFAULT,
RawFrigateCardConfig, RawFrigateCardConfig,
RawFrigateCardConfigArray, RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX, 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 = [ const UPGRADES = [
// v1.2.1 -> v2.0.0 // v1.2.1 -> v2.0.0
upgradeMoveTo('frigate_url', 'frigate.url'), upgradeMoveTo('frigate_url', 'frigate.url'),
@@ -539,4 +564,14 @@ const UPGRADES = [
), ),
upgradeWithOverrides('event_gallery.min_columns', deleteProperty), upgradeWithOverrides('event_gallery.min_columns', deleteProperty),
upgradeMenuModeToStyleAndPosition(), 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_ICON = `${CONF_CAMERAS}.#.icon` as const;
export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY = export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY =
`${CONF_CAMERAS}.#.webrtc_card.entity` as const; `${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 = export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER =
`${CONF_CAMERAS}.#.live_provider` as const; `${CONF_CAMERAS}.#.live_provider` as const;
export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS = 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 = 'event_gallery' as const;
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS = 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 = 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 = export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE =
`${CONF_EVENT_GALLERY}.controls.thumbnails.size` as const; `${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 = export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE =
`${CONF_EVENT_VIEWER}.controls.thumbnails.mode` as const; `${CONF_EVENT_VIEWER}.controls.thumbnails.mode` as const;
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS = 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 = 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 = export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE =
`${CONF_EVENT_VIEWER}.controls.thumbnails.size` as const; `${CONF_EVENT_VIEWER}.controls.thumbnails.size` as const;
export const CONF_EVENT_VIEWER_CONTROLS_TITLE_MODE = 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 = export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE =
`${CONF_LIVE}.controls.thumbnails.size` as const; `${CONF_LIVE}.controls.thumbnails.size` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS = 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 = 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_MODE = `${CONF_LIVE}.controls.title.mode` as const;
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS = export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_LIVE}.controls.title.duration_seconds` as const; `${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 = 'timeline' as const;
export const CONF_TIMELINE_WINDOW_SECONDS = `${CONF_TIMELINE}.window_seconds` 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_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_MODE =
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE = `${CONF_TIMELINE}.controls.thumbnails.size` as const; `${CONF_TIMELINE}.controls.thumbnails.mode` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS = `${CONF_TIMELINE}.controls.thumbnails.show_details` 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 = 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 = 'menu' as const;
export const CONF_MENU_ALIGNMENT = `${CONF_MENU}.alignment` 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_POSITION = `${CONF_MENU}.position` as const;
export const CONF_MENU_STYLE = `${CONF_MENU}.style` 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 = `${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_UI = `${CONF_MENU}.buttons.frigate_ui` as const;
export const CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN = export const CONF_MENU_BUTTONS_FULLSCREEN = `${CONF_MENU}.buttons.fullscreen` as const;
`${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_IMAGE = `${CONF_MENU}.buttons.image` 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 = 'dimensions' as const;
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` 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 { localize } from './localize/localize.js';
import { import {
BUTTON_SIZE_MIN, BUTTON_SIZE_MIN,
FRIGATE_MENU_PRIORITY_MAX,
RawFrigateCardConfig, RawFrigateCardConfig,
RawFrigateCardConfigArray, RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX, THUMBNAIL_WIDTH_MAX,
@@ -67,9 +68,9 @@ import {
CONF_MENU_ALIGNMENT, CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTONS_CLIPS, CONF_MENU_BUTTONS_CLIPS,
CONF_MENU_BUTTONS_FRIGATE, CONF_MENU_BUTTONS_FRIGATE,
CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD, CONF_MENU_BUTTONS_DOWNLOAD,
CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN,
CONF_MENU_BUTTONS_FRIGATE_UI, CONF_MENU_BUTTONS_FRIGATE_UI,
CONF_MENU_BUTTONS_FULLSCREEN,
CONF_MENU_BUTTONS_IMAGE, CONF_MENU_BUTTONS_IMAGE,
CONF_MENU_BUTTONS_LIVE, CONF_MENU_BUTTONS_LIVE,
CONF_MENU_BUTTONS_SNAPSHOTS, 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_POSITION, this._menuPositions)}
${this._renderOptionSelector(CONF_MENU_ALIGNMENT, this._menuAlignments)} ${this._renderOptionSelector(CONF_MENU_ALIGNMENT, this._menuAlignments)}
${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, BUTTON_SIZE_MIN)} ${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, BUTTON_SIZE_MIN)}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_FRIGATE, CONF_MENU_BUTTONS_FRIGATE,
defaults.menu.buttons.frigate, 0,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE), FRIGATE_MENU_PRIORITY_MAX,
)} )}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_LIVE, CONF_MENU_BUTTONS_LIVE,
defaults.menu.buttons.live, 0,
getShowButtonLabel('view.views.live'), FRIGATE_MENU_PRIORITY_MAX,
)} )}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_CLIPS, CONF_MENU_BUTTONS_CLIPS,
defaults.menu.buttons.clips, 0,
getShowButtonLabel('view.views.clips'), FRIGATE_MENU_PRIORITY_MAX,
)} )}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_SNAPSHOTS, CONF_MENU_BUTTONS_SNAPSHOTS,
defaults.menu.buttons.snapshots, 0,
getShowButtonLabel('view.views.snapshots'), FRIGATE_MENU_PRIORITY_MAX,
)} )}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_IMAGE, CONF_MENU_BUTTONS_IMAGE,
defaults.menu.buttons.image, 0,
getShowButtonLabel('view.views.image'), FRIGATE_MENU_PRIORITY_MAX,
)} )}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD, CONF_MENU_BUTTONS_DOWNLOAD,
defaults.menu.buttons.download, 0,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD), FRIGATE_MENU_PRIORITY_MAX,
)} )}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_FRIGATE_UI, CONF_MENU_BUTTONS_FRIGATE_UI,
defaults.menu.buttons.frigate_ui, 0,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_UI), FRIGATE_MENU_PRIORITY_MAX,
)} )}
${this._renderSwitch( ${this._renderNumberInput(
CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN, CONF_MENU_BUTTONS_FULLSCREEN,
defaults.menu.buttons.fullscreen, 0,
getShowButtonLabel(CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN), FRIGATE_MENU_PRIORITY_MAX,
)} )}
</div> </div>
` `
+82 -32
View File
@@ -11,44 +11,77 @@
display: flex; display: flex;
flex-direction: row; 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']) { :host([data-style='outside']) {
width: 100%; width: 100%;
background: var(--secondary-background-color); 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-left-radius: var(--ha-card-border-radius, 4px);
border-top-right-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-left-radius: var(--ha-card-border-radius, 4px);
border-bottom-right-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']), * Positioning for absolute menu styles
:host([data-position='right'][data-alignment='top']) { **************************************/
: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; top: 0px;
} }
:host([data-position='bottom']), :host(:not([data-style='outside'])[data-position='bottom']),
:host([data-position='left'][data-alignment='bottom']), :host(:not([data-style='outside'])[data-position='left'][data-alignment='bottom']),
:host([data-position='right'][data-alignment='bottom']) { :host(:not([data-style='outside'])[data-position='right'][data-alignment='bottom']) {
bottom: 0px; bottom: 0px;
} }
:host([data-position='left']), :host(:not([data-style='outside'])[data-position='left']),
:host([data-position='top'][data-alignment='left']), :host(:not([data-style='outside'])[data-position='top'][data-alignment='left']),
:host([data-position='bottom'][data-alignment='left']) { :host(:not([data-style='outside'])[data-position='bottom'][data-alignment='left']) {
left: 0px; left: 0px;
} }
:host([data-position='right']), :host(:not([data-style='outside'])[data-position='right']),
:host([data-position='top'][data-alignment='right']), :host(:not([data-style='outside'])[data-position='top'][data-alignment='right']),
:host([data-position='bottom'][data-alignment='right']) { :host(:not([data-style='outside'])[data-position='bottom'][data-alignment='right']) {
right: 0px; 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 // Awful hack: Flexbox column wrapping doesn't work properly in most major
// browsers -- the element boundary does not expand to cover the full wrapped // browsers -- the element boundary does not expand to cover the full wrapped
// content as it should. This results in the wrapped 'content' appearing // 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 // - https://bugs.chromium.org/p/chromium/issues/detail?id=507397
writing-mode: vertical-lr; writing-mode: vertical-lr;
} }
:host([data-position='right']) { :host(:not([data-style='outside'])[data-position='right']) {
// See "Awful hack" above. // See "Awful hack" above.
writing-mode: vertical-rl; 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']), * "Reverse" alignments
:host([data-position='top'][data-alignment='right']), **********************/
:host([data-position='bottom'][data-alignment='right']) {
: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; 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 // If the menu has more content that allows, "wrap upwards" to keep the
// Frigate button in the same place. // Frigate button in the same place.
flex-wrap: wrap-reverse; flex-wrap: wrap-reverse;
} }
/********************************************
* Positioning for absolute based menu styles
********************************************/
:host([data-style='overlay']), :host([data-style='overlay']),
:host([data-style='hover']), :host([data-style='hover']),
:host([data-style='hidden']) { :host([data-style='hidden']) {
@@ -111,13 +171,3 @@
overflow: visible; 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-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
View File
@@ -53,21 +53,13 @@ const FRIGATE_CARD_VIEWS = [
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number]; export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
const FRIGATE_MENU_STYLES = [ const FRIGATE_MENU_STYLES = ['none', 'hidden', 'overlay', 'hover', 'outside'] as const;
'none', const FRIGATE_MENU_POSITIONS = ['left', 'right', 'top', 'bottom'] as const;
'hidden',
'overlay',
'hover',
'outside',
] as const;
const FRIGATE_MENU_POSITIONS = [
'left',
'right',
'top',
'bottom',
] as const;
const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS; 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; const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number]; export type LiveProvider = typeof LIVE_PROVIDERS[number];
@@ -336,14 +328,23 @@ export type CameraConfig = z.infer<typeof cameraConfigSchema>;
* Custom Element Types. * Custom Element Types.
*/ */
export const menuIconSchema = iconSchema.extend({ const menuBaseSchema = z.object({
type: z.literal('custom:frigate-card-menu-icon'), 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 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'), type: z.literal('custom:frigate-card-menu-state-icon'),
}); })
.merge(menuBaseSchema);
export type MenuStateIcon = z.infer<typeof menuStateIconSchema>; export type MenuStateIcon = z.infer<typeof menuStateIconSchema>;
const menuSubmenuItemSchema = elementsBaseSchema.extend({ const menuSubmenuItemSchema = elementsBaseSchema.extend({
@@ -354,11 +355,14 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
}); });
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>; 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'), type: z.literal('custom:frigate-card-menu-submenu'),
items: menuSubmenuItemSchema.array(), items: menuSubmenuItemSchema.array(),
}); })
.merge(menuBaseSchema);
export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>; export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu;
const frigateCardConditionSchema = z.object({ const frigateCardConditionSchema = z.object({
view: z.string().array().optional(), view: z.string().array().optional(),
@@ -617,16 +621,16 @@ const menuConfigDefault = {
position: 'top' as const, position: 'top' as const,
alignment: 'left' as const, alignment: 'left' as const,
buttons: { buttons: {
frigate: true, frigate: FRIGATE_MENU_PRIORITY_DEFAULT,
cameras: true, cameras: FRIGATE_MENU_PRIORITY_DEFAULT,
live: true, live: FRIGATE_MENU_PRIORITY_DEFAULT,
clips: true, clips: FRIGATE_MENU_PRIORITY_DEFAULT,
snapshots: true, snapshots: FRIGATE_MENU_PRIORITY_DEFAULT,
image: false, image: 0,
timeline: true, timeline: FRIGATE_MENU_PRIORITY_DEFAULT,
download: true, download: FRIGATE_MENU_PRIORITY_DEFAULT,
frigate_ui: true, frigate_ui: FRIGATE_MENU_PRIORITY_DEFAULT,
fullscreen: true, fullscreen: FRIGATE_MENU_PRIORITY_DEFAULT,
}, },
button_size: 40, button_size: 40,
}; };
@@ -638,16 +642,56 @@ const menuConfigSchema = z
alignment: z.enum(FRIGATE_MENU_ALIGNMENTS).default(menuConfigDefault.alignment), alignment: z.enum(FRIGATE_MENU_ALIGNMENTS).default(menuConfigDefault.alignment),
buttons: z buttons: z
.object({ .object({
frigate: z.boolean().default(menuConfigDefault.buttons.frigate), frigate: z
cameras: z.boolean().default(menuConfigDefault.buttons.cameras), .number()
live: z.boolean().default(menuConfigDefault.buttons.live), .min(0)
clips: z.boolean().default(menuConfigDefault.buttons.clips), .max(FRIGATE_MENU_PRIORITY_MAX)
snapshots: z.boolean().default(menuConfigDefault.buttons.snapshots), .default(menuConfigDefault.buttons.frigate),
image: z.boolean().default(menuConfigDefault.buttons.image), cameras: z
timeline: z.boolean().default(menuConfigDefault.buttons.timeline), .number()
download: z.boolean().default(menuConfigDefault.buttons.download), .min(0)
frigate_ui: z.boolean().default(menuConfigDefault.buttons.frigate_ui), .max(FRIGATE_MENU_PRIORITY_MAX)
fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen), .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), .default(menuConfigDefault.buttons),
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size), button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),