From 267ef30ba12e73aa88d1efe82d0b285cd1e8fe3e Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 17 Apr 2022 09:01:05 -0700 Subject: [PATCH] Automated upgrade of non-px units. --- README.md | 14 ++-- src/components/gallery.ts | 2 - src/config-mgmt.ts | 135 +++++++++++++++++++++++++++----------- src/editor.ts | 13 +++- src/types.ts | 9 ++- 5 files changed, 121 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index b801404c..0b56faa6 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ menu: | Option | Default | Overridable | Description | | - | - | - | - | | `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. | #### Menu Options: Buttons @@ -258,7 +258,7 @@ live: | 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`).| -| `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_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.| @@ -276,7 +276,7 @@ live: | 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` . | -| `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`. | @@ -328,7 +328,7 @@ event_viewer: | 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` . | -| `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 @@ -343,7 +343,7 @@ event_viewer: | 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`).| -| `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_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 | | - | - | - | - | -| `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_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.| @@ -425,7 +425,7 @@ timeline: | 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`).| -| `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_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.| diff --git a/src/components/gallery.ts b/src/components/gallery.ts index 8509fa0b..aa472d2b 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -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 /* eslint-disable @typescript-eslint/no-explicit-any */ diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts index e2aec88b..7bde7990 100644 --- a/src/config-mgmt.ts +++ b/src/config-mgmt.ts @@ -1,4 +1,4 @@ -import { get, set } from 'lodash-es'; +import { get, isEqual, set } from 'lodash-es'; import { CONF_CAMERAS, CONF_CAMERAS_ARRAY_CAMERA_ENTITY, @@ -11,7 +11,10 @@ import { CONF_EVENT_VIEWER_AUTO_PLAY, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, + CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_IMAGE_URL, + CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, + CONF_LIVE_CONTROLS_THUMBNAILS_SIZE, CONF_LIVE_PRELOAD, CONF_LIVE_WEBRTC_CARD, CONF_MENU, @@ -22,7 +25,13 @@ import { CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_UPDATE_ENTITIES, } 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. @@ -131,7 +140,7 @@ export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardCo * @param value The value. * @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; }; @@ -140,10 +149,46 @@ const isNotObject = function (value: unknown) { * @param value The value. * @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); }; +/** + * 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. * @param obj The configuration object in which the property resides. @@ -158,13 +203,21 @@ export const moveConfigValue = ( newPath: string, transform?: (valueIn: unknown) => unknown, ): boolean => { - let value = getConfigValue(obj, oldPath); - if (transform) { - value = transform(value); + const inValue = getConfigValue(obj, oldPath); + if (inValue === undefined) { + 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); - setConfigValue(obj, newPath, value); + return true; + } + if (outValue !== undefined) { + deleteConfigValue(obj, oldPath); + setConfigValue(obj, newPath, outValue); return true; } 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 * 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. * @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 upgradeArrayValue( CONF_CAMERAS, - upgradeChangeIfPresent('live_provider', (val) => + upgradeWithOverrides('live_provider', (val) => val === 'frigate' ? 'ha' : val === 'webrtc' ? 'webrtc-card' : val, ), ), upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('webrtc', 'webrtc_card')), upgradeMoveToWithOverrides('live.webrtc', CONF_LIVE_WEBRTC_CARD), 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), + ), ]; diff --git a/src/editor.ts b/src/editor.ts index 3916781a..af83bfaa 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -5,6 +5,7 @@ import { customElement, property, state } from 'lit/decorators.js'; import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; import { localize } from './localize/localize.js'; import { + BUTTON_SIZE_MIN, RawFrigateCardConfig, RawFrigateCardConfigArray, THUMBNAIL_WIDTH_MAX, @@ -847,7 +848,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ? html`
${this._renderOptionSelector(CONF_MENU_MODE, this._menuModes)} - ${this._renderNumberInput(CONF_MENU_BUTTON_SIZE)} + ${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, BUTTON_SIZE_MIN)} ${this._renderSwitch( CONF_MENU_BUTTONS_FRIGATE, defaults.menu.buttons.frigate, @@ -904,7 +905,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, this._liveNextPreviousControlStyles, )} - ${this._renderNumberInput(CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE)} + ${this._renderNumberInput( + CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, + BUTTON_SIZE_MIN, + )} ${this._renderOptionSelector( CONF_LIVE_CONTROLS_THUMBNAILS_MODE, this._thumbnailModes, @@ -983,7 +987,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, 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( CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE, this._thumbnailModes, diff --git a/src/types.ts b/src/types.ts index 5a27e7d9..df87f2cf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,10 @@ import { z } from 'zod'; 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_MIN = 75; @@ -455,7 +458,7 @@ export type ThumbnailsControlConfig = z.infer; const nextPreviousControlConfigSchema = z.object({ style: z.enum(['none', 'chevrons', 'icons', 'thumbnails']), - size: z.number().min(1), + size: z.number().min(BUTTON_SIZE_MIN), }); export type NextPreviousControlConfig = z.infer; @@ -638,7 +641,7 @@ const menuConfigSchema = z fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen), }) .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); export type MenuConfig = z.infer;