Automated upgrade of non-px units.

This commit is contained in:
Dermot Duffy
2022-04-17 09:01:05 -07:00
parent 13d7db341b
commit 267ef30ba1
5 changed files with 121 additions and 52 deletions
+7 -7
View File
@@ -170,7 +170,7 @@ menu:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `mode` | `hidden-top` | :white_check_mark: | The menu mode to show by default. See [menu modes](#menu-modes) below.| | `mode` | `hidden-top` | :white_check_mark: | The menu mode to show by default. See [menu modes](#menu-modes) below.|
| `button_size` | 40 | :white_check_mark: | The size of the menu buttons in pixels.| | `button_size` | 40 | :white_check_mark: | The size of the menu buttons in pixels. Must be >= `20`.|
| `buttons` | | :white_check_mark: | Whether to show or hide built-in buttons. See below. | | `buttons` | | :white_check_mark: | Whether to show or hide built-in buttons. See below. |
#### Menu Options: Buttons #### Menu Options: Buttons
@@ -258,7 +258,7 @@ live:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `mode` | `none` | :white_check_mark: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).| | `mode` | `none` | :white_check_mark: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `size` | 100 | :white_check_mark: | The size of the thumbnails in the thumbnail carousel in pixels.| | `size` | 100 | :white_check_mark: | The size of the thumbnails in the thumbnail carousel in pixels. Must be >= `75` and <= `175`. |
| `show_details` | `false` | :white_check_mark: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :white_check_mark: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_controls` | `true` | :white_check_mark: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.| | `show_controls` | `true` | :white_check_mark: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
| `media` | `clips` | :white_check_mark: | Whether to show `clips` or `snapshots` in the thumbnail carousel in the `live` view.| | `media` | `clips` | :white_check_mark: | Whether to show `clips` or `snapshots` in the thumbnail carousel in the `live` view.|
@@ -276,7 +276,7 @@ live:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `style` | `chevrons` | :white_check_mark: | When viewing live cameras, what kind of controls to show to move to the previous/next camera. Acceptable values: `chevrons`, `icons`, `none` . | | `style` | `chevrons` | :white_check_mark: | When viewing live cameras, what kind of controls to show to move to the previous/next camera. Acceptable values: `chevrons`, `icons`, `none` . |
| `size` | 48 | :white_check_mark: | The size of the next/previous controls in pixels.| | `size` | 48 | :white_check_mark: | The size of the next/previous controls in pixels. Must be >= `20`. |
<a name="live-controls-title"></a> <a name="live-controls-title"></a>
@@ -328,7 +328,7 @@ event_viewer:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `style` | `thumbnails` | :heavy_multiplication_x: | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . | | `style` | `thumbnails` | :heavy_multiplication_x: | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
| `size` | 48 | :heavy_multiplication_x: | The size of the next/previous controls in pixels.| | `size` | 48 | :heavy_multiplication_x: | The size of the next/previous controls in pixels. Must be >= `20`.|
#### Event Viewer Controls: Thumbnails #### Event Viewer Controls: Thumbnails
@@ -343,7 +343,7 @@ event_viewer:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).| | `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel pixels.| | `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel pixels. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.| | `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
@@ -374,7 +374,7 @@ event_gallery:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the event gallery in pixels.| | `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the event gallery in pixels. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.| | `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
| `actions` | | :heavy_multiplication_x: | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.| | `actions` | | :heavy_multiplication_x: | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.|
@@ -425,7 +425,7 @@ timeline:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).| | `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel in pixels.| | `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel in pixels. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.| | `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
-2
View File
@@ -1,5 +1,3 @@
// TODO: Add details & controls & size support
// TODO: automatic upgrade for thumbnail sizes in px and menu buttons in px
// TODO: automatic remove of min_columns // TODO: automatic remove of min_columns
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
+98 -37
View File
@@ -1,4 +1,4 @@
import { get, set } from 'lodash-es'; import { get, isEqual, set } from 'lodash-es';
import { import {
CONF_CAMERAS, CONF_CAMERAS,
CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
@@ -11,7 +11,10 @@ import {
CONF_EVENT_VIEWER_AUTO_PLAY, CONF_EVENT_VIEWER_AUTO_PLAY,
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
CONF_IMAGE_URL, CONF_IMAGE_URL,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
CONF_LIVE_PRELOAD, CONF_LIVE_PRELOAD,
CONF_LIVE_WEBRTC_CARD, CONF_LIVE_WEBRTC_CARD,
CONF_MENU, CONF_MENU,
@@ -22,7 +25,13 @@ import {
CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_TIMEOUT_SECONDS,
CONF_VIEW_UPDATE_ENTITIES, CONF_VIEW_UPDATE_ENTITIES,
} from './const'; } from './const';
import { RawFrigateCardConfig, RawFrigateCardConfigArray } from './types'; import {
BUTTON_SIZE_MIN,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX,
THUMBNAIL_WIDTH_MIN,
} from './types';
/** /**
* Set a configuration value. * Set a configuration value.
@@ -131,7 +140,7 @@ export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardCo
* @param value The value. * @param value The value.
* @returns `true` is the value is not an object. * @returns `true` is the value is not an object.
*/ */
const isNotObject = function (value: unknown) { const isNotObject = function (value: unknown): unknown | undefined {
return typeof value !== 'object' ? value : undefined; return typeof value !== 'object' ? value : undefined;
}; };
@@ -140,10 +149,46 @@ const isNotObject = function (value: unknown) {
* @param value The value. * @param value The value.
* @returns A number or undefined. * @returns A number or undefined.
*/ */
const toNumberOrIgnore = function (value: unknown) { const toNumberOrIgnore = function (value: unknown): number | undefined {
return isNaN(value as number) ? undefined : Number(value); return isNaN(value as number) ? undefined : Number(value);
}; };
/**
* Create a transform that will cap a numeric value.
* @param value The value.
* @returns A number or null.
*/
const createRangedTransform = function (
transform: (value: unknown) => unknown,
min?: number,
max?: number,
): (valueIn: unknown) => unknown {
return (value: unknown): unknown => {
let transformed = transform(value);
if (typeof transformed !== 'number') {
return transformed;
}
transformed = min ? Math.max(min, transformed as number) : transformed;
transformed = max ? Math.min(max, transformed as number) : transformed;
return transformed;
};
};
const toPixelsOrDelete = function (value: unknown): number | null | undefined {
// Ignore the value if it's a number.
if (typeof value === 'number') {
return undefined;
}
// Delete the value if it's not a string.
if (typeof value !== 'string') {
return null;
}
// Remove 'px' and return the number, unless it's an invalid number -- then
// delete it.
value = value.replace(/px$/i, '');
return isNaN(value as number) ? null : Number(value);
};
/** /**
* Move a property from one location to another. * Move a property from one location to another.
* @param obj The configuration object in which the property resides. * @param obj The configuration object in which the property resides.
@@ -158,13 +203,21 @@ export const moveConfigValue = (
newPath: string, newPath: string,
transform?: (valueIn: unknown) => unknown, transform?: (valueIn: unknown) => unknown,
): boolean => { ): boolean => {
let value = getConfigValue(obj, oldPath); const inValue = getConfigValue(obj, oldPath);
if (transform) { if (inValue === undefined) {
value = transform(value); return false;
} }
if (typeof value !== 'undefined') { const outValue = transform ? transform(inValue) : inValue;
if (oldPath === newPath && isEqual(inValue, outValue)) {
return false;
}
if (outValue === null) {
deleteConfigValue(obj, oldPath); deleteConfigValue(obj, oldPath);
setConfigValue(obj, newPath, value); return true;
}
if (outValue !== undefined) {
deleteConfigValue(obj, oldPath);
setConfigValue(obj, newPath, outValue);
return true; return true;
} }
return false; return false;
@@ -197,33 +250,6 @@ const upgradeMoveTo = function (
}; };
}; };
/**
* Upgrade a property by changing it if it is present.
* @param path The property path.
* @param transform A callback that transforms the old value to the new value,
* if undefined is returned the property is removed.
* @returns `true` if the configuration was modified.
*/
const upgradeChangeIfPresent = function (
path: string,
transform: (valueIn: unknown) => unknown,
): (obj: RawFrigateCardConfig) => boolean {
return function (obj: RawFrigateCardConfig): boolean {
const oldValue = getConfigValue(obj, path);
if (oldValue !== undefined) {
const newValue = transform(oldValue);
if (newValue === undefined) {
deleteConfigValue(obj, path);
return true;
} else if (newValue !== oldValue) {
setConfigValue(obj, path, newValue);
return true;
}
}
return false;
};
};
/** /**
* Upgrade by moving a property from one location to another, and moving a * Upgrade by moving a property from one location to another, and moving a
* property specified in a top-level overrides object. * property specified in a top-level overrides object.
@@ -249,6 +275,19 @@ const upgradeMoveToWithOverrides = function (
}; };
}; };
/**
* Upgrade a property in place with overrides.
* @param path The old property path.
* @param transform An optional transform for the value.
* @returns A function that returns `true` if the configuration was modified.
*/
const upgradeWithOverrides = function (
path: string,
transform?: (valueIn: unknown) => unknown,
): (obj: RawFrigateCardConfig) => boolean {
return upgradeMoveToWithOverrides(path, path, transform);
};
/** /**
* Given a path to an array, apply an upgrade to each object in the array. * Given a path to an array, apply an upgrade to each object in the array.
* @param arrayPath The path to the array to upgrade. * @param arrayPath The path to the array to upgrade.
@@ -377,11 +416,33 @@ const UPGRADES = [
// v3.0.0-rc.1 -> v3.0.0-rc.2 // v3.0.0-rc.1 -> v3.0.0-rc.2
upgradeArrayValue( upgradeArrayValue(
CONF_CAMERAS, CONF_CAMERAS,
upgradeChangeIfPresent('live_provider', (val) => upgradeWithOverrides('live_provider', (val) =>
val === 'frigate' ? 'ha' : val === 'webrtc' ? 'webrtc-card' : val, val === 'frigate' ? 'ha' : val === 'webrtc' ? 'webrtc-card' : val,
), ),
), ),
upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('webrtc', 'webrtc_card')), upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('webrtc', 'webrtc_card')),
upgradeMoveToWithOverrides('live.webrtc', CONF_LIVE_WEBRTC_CARD), upgradeMoveToWithOverrides('live.webrtc', CONF_LIVE_WEBRTC_CARD),
upgradeMoveToWithOverrides('image.src', CONF_IMAGE_URL), upgradeMoveToWithOverrides('image.src', CONF_IMAGE_URL),
// v3.0.0 -> v4.0.0-rc.1
upgradeWithOverrides(
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
),
upgradeWithOverrides(
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
),
upgradeWithOverrides(
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
),
upgradeWithOverrides(
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
),
upgradeWithOverrides(
CONF_MENU_BUTTON_SIZE,
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
),
]; ];
+10 -3
View File
@@ -5,6 +5,7 @@ import { customElement, property, state } from 'lit/decorators.js';
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import { import {
BUTTON_SIZE_MIN,
RawFrigateCardConfig, RawFrigateCardConfig,
RawFrigateCardConfigArray, RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX, THUMBNAIL_WIDTH_MAX,
@@ -847,7 +848,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html` ? html`
<div class="values"> <div class="values">
${this._renderOptionSelector(CONF_MENU_MODE, this._menuModes)} ${this._renderOptionSelector(CONF_MENU_MODE, this._menuModes)}
${this._renderNumberInput(CONF_MENU_BUTTON_SIZE)} ${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, BUTTON_SIZE_MIN)}
${this._renderSwitch( ${this._renderSwitch(
CONF_MENU_BUTTONS_FRIGATE, CONF_MENU_BUTTONS_FRIGATE,
defaults.menu.buttons.frigate, defaults.menu.buttons.frigate,
@@ -904,7 +905,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
this._liveNextPreviousControlStyles, this._liveNextPreviousControlStyles,
)} )}
${this._renderNumberInput(CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE)} ${this._renderNumberInput(
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
BUTTON_SIZE_MIN,
)}
${this._renderOptionSelector( ${this._renderOptionSelector(
CONF_LIVE_CONTROLS_THUMBNAILS_MODE, CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
this._thumbnailModes, this._thumbnailModes,
@@ -983,7 +987,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
this._eventViewerNextPreviousControlStyles, this._eventViewerNextPreviousControlStyles,
)} )}
${this._renderNumberInput(CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE)} ${this._renderNumberInput(
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
BUTTON_SIZE_MIN,
)}
${this._renderOptionSelector( ${this._renderOptionSelector(
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE, CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
this._thumbnailModes, this._thumbnailModes,
+6 -3
View File
@@ -14,7 +14,10 @@ import { z } from 'zod';
import { deepRemoveDefaults } from './zod-util'; import { deepRemoveDefaults } from './zod-util';
// The maximum width thumbnail Frigate returns // The min allowed size of buttons.
export const BUTTON_SIZE_MIN = 20;
// The min/max width thumbnail (Frigate returns a maximum of 175px).
export const THUMBNAIL_WIDTH_MAX = 175; export const THUMBNAIL_WIDTH_MAX = 175;
export const THUMBNAIL_WIDTH_MIN = 75; export const THUMBNAIL_WIDTH_MIN = 75;
@@ -455,7 +458,7 @@ export type ThumbnailsControlConfig = z.infer<typeof thumbnailsControlSchema>;
const nextPreviousControlConfigSchema = z.object({ const nextPreviousControlConfigSchema = z.object({
style: z.enum(['none', 'chevrons', 'icons', 'thumbnails']), style: z.enum(['none', 'chevrons', 'icons', 'thumbnails']),
size: z.number().min(1), size: z.number().min(BUTTON_SIZE_MIN),
}); });
export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>; export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>;
@@ -638,7 +641,7 @@ const menuConfigSchema = z
fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen), fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen),
}) })
.default(menuConfigDefault.buttons), .default(menuConfigDefault.buttons),
button_size: z.number().min(1).default(menuConfigDefault.button_size), button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
}) })
.default(menuConfigDefault); .default(menuConfigDefault);
export type MenuConfig = z.infer<typeof menuConfigSchema>; export type MenuConfig = z.infer<typeof menuConfigSchema>;