From 064c51f487f1564783cdcfd475c870443b34dfe6 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 29 May 2024 05:32:53 -0700 Subject: [PATCH] Add a picture uploader to picture-card-editor (#18695) * Add a picture uploader to picture-card-editor * add imageSelector * lint * Add delete button to picture-upload * updates from feedback * fix lint * Update en.json * Update selector.ts * remove delete --- src/components/ha-picture-upload.ts | 72 +++++---- .../ha-selector/ha-selector-image.ts | 143 ++++++++++++++++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/image_upload.ts | 16 +- src/data/selector.ts | 6 + .../hui-picture-card-editor.ts | 2 +- .../hui-picture-entity-card-editor.ts | 2 +- .../hui-picture-glance-card-editor.ts | 2 +- src/resources/styles.ts | 1 + src/translations/en.json | 10 ++ 10 files changed, 218 insertions(+), 37 deletions(-) create mode 100644 src/components/ha-selector/ha-selector-image.ts diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index 748da39172..efb96c1eb2 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -2,6 +2,7 @@ import { mdiImagePlus } from "@mdi/js"; import { LitElement, TemplateResult, css, html } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; +import { haStyle } from "../resources/styles"; import { createImage, generateImageThumbnailUrl } from "../data/image_upload"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { @@ -62,13 +63,15 @@ export class HaPictureUpload extends LitElement { alt=${this.currentImageAltText || this.hass.localize("ui.components.picture-upload.current_image_alt")} /> - - +
+ + +
`; } @@ -140,32 +143,35 @@ export class HaPictureUpload extends LitElement { } static get styles() { - return css` - :host { - display: block; - height: 240px; - } - ha-file-upload { - height: 100%; - } - .center-vertical { - display: flex; - align-items: center; - height: 100%; - } - .value { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - } - img { - max-width: 100%; - max-height: 200px; - margin-bottom: 4px; - border-radius: var(--file-upload-image-border-radius); - } - `; + return [ + haStyle, + css` + :host { + display: block; + height: 240px; + } + ha-file-upload { + height: 100%; + } + .center-vertical { + display: flex; + align-items: center; + height: 100%; + } + .value { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + img { + max-width: 100%; + max-height: 200px; + margin-bottom: 4px; + border-radius: var(--file-upload-image-border-radius); + } + `, + ]; } } diff --git a/src/components/ha-selector/ha-selector-image.ts b/src/components/ha-selector/ha-selector-image.ts new file mode 100644 index 0000000000..80eccaf787 --- /dev/null +++ b/src/components/ha-selector/ha-selector-image.ts @@ -0,0 +1,143 @@ +import { css, CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { ImageSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-icon-button"; +import "../ha-textarea"; +import "../ha-textfield"; +import "../ha-picture-upload"; +import "../ha-radio"; +import type { HaPictureUpload } from "../ha-picture-upload"; +import { URL_PREFIX } from "../../data/image_upload"; + +@customElement("ha-selector-image") +export class HaImageSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public value?: any; + + @property() public name?: string; + + @property() public label?: string; + + @property() public placeholder?: string; + + @property() public helper?: string; + + @property({ attribute: false }) public selector!: ImageSelector; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @state() private showUpload = false; + + protected firstUpdated(changedProps): void { + super.firstUpdated(changedProps); + + if (!this.value || this.value.startsWith(URL_PREFIX)) { + this.showUpload = true; + } + } + + protected render() { + return html` +
+ + ${!this.showUpload + ? html` + + ` + : html` + + `} +
+ `; + } + + private _radioGroupPicked(ev): void { + this.showUpload = ev.target.value === "upload"; + } + + private _pictureChanged(ev) { + const value = (ev.target as HaPictureUpload).value; + + fireEvent(this, "value-changed", { value: value ?? undefined }); + } + + private _handleChange(ev) { + let value = ev.target.value; + if (this.value === value) { + return; + } + if (value === "" && !this.required) { + value = undefined; + } + + fireEvent(this, "value-changed", { value }); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: block; + position: relative; + } + div { + display: flex; + flex-direction: column; + } + label { + display: flex; + flex-direction: column; + } + ha-textarea, + ha-textfield { + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-image": HaImageSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index e622721b6d..8cab4393b8 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -32,6 +32,7 @@ const LOAD_ELEMENTS = { file: () => import("./ha-selector-file"), floor: () => import("./ha-selector-floor"), label: () => import("./ha-selector-label"), + image: () => import("./ha-selector-image"), language: () => import("./ha-selector-language"), navigation: () => import("./ha-selector-navigation"), number: () => import("./ha-selector-number"), diff --git a/src/data/image_upload.ts b/src/data/image_upload.ts index 0d549e8ccf..d9e93095f5 100644 --- a/src/data/image_upload.ts +++ b/src/data/image_upload.ts @@ -8,10 +8,24 @@ interface Image { id: string; } +export const URL_PREFIX = "/api/image/serve/"; + export interface ImageMutableParams { name: string; } +export const getIdFromUrl = (url: string): string | undefined => { + let id; + if (url.startsWith(URL_PREFIX)) { + id = url.substring(URL_PREFIX.length); + const idx = id.indexOf("/"); + if (idx >= 0) { + id = id.substring(0, idx); + } + } + return id; +}; + export const generateImageThumbnailUrl = ( mediaId: string, size?: number, @@ -61,5 +75,5 @@ export const updateImage = ( export const deleteImage = (hass: HomeAssistant, id: string) => hass.callWS({ type: "image/delete", - media_id: id, + image_id: id, }); diff --git a/src/data/selector.ts b/src/data/selector.ts index 172cf72630..10adc80ba7 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -40,6 +40,7 @@ export type Selector = | FileSelector | IconSelector | LabelSelector + | ImageSelector | LanguageSelector | LocationSelector | MediaSelector @@ -256,6 +257,11 @@ export interface IconSelector { } | null; } +export interface ImageSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + image: {} | null; +} + export interface LabelSelector { label: { multiple?: boolean; diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 1ce62d6a78..20ff12e88b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -24,7 +24,7 @@ const cardConfigStruct = assign( ); const SCHEMA = [ - { name: "image", selector: { text: {} } }, + { name: "image", selector: { image: {} } }, { name: "image_entity", selector: { entity: { domain: "image" } } }, { name: "alt_text", selector: { text: {} } }, { name: "theme", selector: { theme: {} } }, diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts index bf5ffe1539..5cc9d84edf 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts @@ -32,7 +32,7 @@ const cardConfigStruct = assign( const SCHEMA = [ { name: "entity", required: true, selector: { entity: {} } }, { name: "name", selector: { text: {} } }, - { name: "image", selector: { text: {} } }, + { name: "image", selector: { image: {} } }, { name: "camera_image", selector: { entity: { domain: "camera" } } }, { name: "", diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts index 5d4d15c431..1c74428d38 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts @@ -35,7 +35,7 @@ const cardConfigStruct = assign( const SCHEMA = [ { name: "title", selector: { text: {} } }, - { name: "image", selector: { text: {} } }, + { name: "image", selector: { image: {} } }, { name: "image_entity", selector: { entity: { domain: "image" } } }, { name: "camera_image", selector: { entity: { domain: "camera" } } }, { diff --git a/src/resources/styles.ts b/src/resources/styles.ts index a2ed64635e..ba0b56164f 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -82,6 +82,7 @@ export const haStyle = css` color: var(--error-color); } + ha-button.warning, mwc-button.warning { --mdc-theme-primary: var(--error-color); } diff --git a/src/translations/en.json b/src/translations/en.json index 1a04fdcb71..83e115d2ee 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -377,6 +377,11 @@ "upload_failed": "Upload failed", "unknown_file": "Unknown file" }, + "image": { + "select_image": "Select image", + "upload": "Upload picture", + "url": "Local path or web URL" + }, "location": { "latitude": "[%key:ui::panel::config::zone::detail::latitude%]", "longitude": "[%key:ui::panel::config::zone::detail::longitude%]", @@ -412,6 +417,11 @@ "manual": "Manual Entry" } }, + "image": { + "select_image": "Select image", + "upload": "Upload picture", + "url": "Local path or web URL" + }, "text": { "show_password": "Show password", "hide_password": "Hide password"