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"