From 1ce314dc44610daa5bc0e7fbe051f7e80e271ef3 Mon Sep 17 00:00:00 2001 From: karwosts Date: Sat, 30 Nov 2024 10:09:03 -0800 Subject: [PATCH 1/7] Allow selecting previously uploaded image for picture upload --- src/components/ha-picture-upload.ts | 41 ++++++++++++++++++- .../dialog-media-player-browse.ts | 2 +- .../media-player/show-media-browser-dialog.ts | 1 + src/translations/en.json | 3 +- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index b3ff842322ce..b25c042888e4 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 type { TemplateResult } from "lit"; import { LitElement, css, html } from "lit"; import { customElement, property, state } from "lit/decorators"; +import type { MediaPickedEvent } from "../data/media-player"; import { fireEvent } from "../common/dom/fire_event"; import { haStyle } from "../resources/styles"; import { createImage, generateImageThumbnailUrl } from "../data/image_upload"; @@ -12,6 +13,7 @@ import type { HomeAssistant } from "../types"; import "./ha-button"; import "./ha-circular-progress"; import "./ha-file-upload"; +import { showMediaBrowserDialog } from "./media-player/show-media-browser-dialog"; @customElement("ha-picture-upload") export class HaPictureUpload extends LitElement { @@ -53,6 +55,11 @@ export class HaPictureUpload extends LitElement { @change=${this._handleFileCleared} accept="image/png, image/jpeg, image/gif" > + ${this.hass.localize( + "ui.components.picture-upload.select_previous_upload" + )} `; } return html`
@@ -141,16 +148,46 @@ export class HaPictureUpload extends LitElement { } } + private _chooseMedia(): void { + showMediaBrowserDialog(this, { + action: "pick", + entityId: "browser", + navigateIds: [ + { media_content_id: undefined, media_content_type: undefined }, + { + media_content_id: "media-source://image_upload", + media_content_type: "app", + }, + ], + minimumNavigateLevel: 2, + mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { + const id = pickedMedia.item.media_content_id; + const stringToRemove = "media-source://image_upload/"; + if (id.startsWith(stringToRemove)) { + this.value = generateImageThumbnailUrl( + id.substr(stringToRemove.length), + this.size, + this.original + ); + fireEvent(this, "change"); + } + }, + }); + } + static get styles() { return [ haStyle, css` :host { display: block; - height: 240px; } ha-file-upload { - height: 100%; + height: 240px; + } + ha-button.center { + display: flex; + align-items: center; } .center-vertical { display: flex; diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index 93aa4d39d08d..ea657601929d 100644 --- a/src/components/media-player/dialog-media-player-browse.ts +++ b/src/components/media-player/dialog-media-player-browse.ts @@ -85,7 +85,7 @@ class DialogMediaPlayerBrowse extends LitElement { @opened=${this._dialogOpened} > - ${this._navigateIds.length > 1 + ${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1) ? html` void; navigateIds?: MediaPlayerItemId[]; + minimumNavigateLevel?: number; } export const showMediaBrowserDialog = ( diff --git a/src/translations/en.json b/src/translations/en.json index 221ad621bc24..3643651b0bae 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -757,7 +757,8 @@ "change_picture": "Change picture", "current_image_alt": "Current picture", "supported_formats": "Supports JPEG, PNG, or GIF image.", - "unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image." + "unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image.", + "select_previous_upload": "Select previous upload" }, "color-picker": { "default": "default", From a201faab466f4e44ad85df2601b153e99f472045 Mon Sep 17 00:00:00 2001 From: karwosts Date: Sat, 7 Dec 2024 21:09:36 -0800 Subject: [PATCH 2/7] Enable recropping if choosing previous image --- src/components/ha-picture-upload.ts | 39 ++++++++++++++----- .../image-cropper-dialog.ts | 36 ++++++++++++++++- src/translations/en.json | 3 +- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index b25c042888e4..a1499fc73271 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -100,7 +100,7 @@ export class HaPictureUpload extends LitElement { this.value = null; } - private async _cropFile(file: File) { + private async _cropFile(file: File, mediaId?: string) { if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) { showAlertDialog(this, { text: this.hass.localize( @@ -116,7 +116,16 @@ export class HaPictureUpload extends LitElement { aspectRatio: NaN, }, croppedCallback: (croppedFile) => { - this._uploadFile(croppedFile); + if (mediaId && croppedFile === file) { + this.value = generateImageThumbnailUrl( + mediaId, + this.size, + this.original + ); + fireEvent(this, "change"); + } else { + this._uploadFile(croppedFile); + } }, }); } @@ -160,16 +169,28 @@ export class HaPictureUpload extends LitElement { }, ], minimumNavigateLevel: 2, - mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { + mediaPickedCallback: async (pickedMedia: MediaPickedEvent) => { const id = pickedMedia.item.media_content_id; const stringToRemove = "media-source://image_upload/"; if (id.startsWith(stringToRemove)) { - this.value = generateImageThumbnailUrl( - id.substr(stringToRemove.length), - this.size, - this.original - ); - fireEvent(this, "change"); + const mediaId = id.substr(stringToRemove.length); + if (this.crop) { + const url = generateImageThumbnailUrl(mediaId, undefined, true); + const response = await fetch(url); + const data = await response.blob(); + const metadata = { + type: pickedMedia.item.media_content_type, + }; + const file = new File([data], pickedMedia.item.title, metadata); + this._cropFile(file, mediaId); + } else { + this.value = generateImageThumbnailUrl( + mediaId, + this.size, + this.original + ); + fireEvent(this, "change"); + } } }, }); diff --git a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts index 8084fc421af6..280d2c319d41 100644 --- a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts +++ b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts @@ -3,7 +3,7 @@ import Cropper from "cropperjs"; // @ts-ignore import cropperCss from "cropperjs/dist/cropper.css"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; -import { css, html, LitElement, unsafeCSS } from "lit"; +import { css, html, nothing, LitElement, unsafeCSS } from "lit"; import { customElement, property, state, query } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import "../../components/ha-dialog"; @@ -23,6 +23,8 @@ export class HaImagecropperDialog extends LitElement { private _cropper?: Cropper; + @state() private _isTargetAspectRatio?: boolean; + public showDialog(params: HaImageCropperDialogParams): void { this._params = params; this._open = true; @@ -33,6 +35,7 @@ export class HaImagecropperDialog extends LitElement { this._params = undefined; this._cropper?.destroy(); this._cropper = undefined; + this._isTargetAspectRatio = false; } protected updated(changedProperties: PropertyValues) { @@ -47,6 +50,7 @@ export class HaImagecropperDialog extends LitElement { dragMode: "move", minCropBoxWidth: 50, ready: () => { + this._isTargetAspectRatio = this._checkMatchAspectRatio(); URL.revokeObjectURL(this._image!.src); }, }); @@ -55,6 +59,25 @@ export class HaImagecropperDialog extends LitElement { } } + private _checkMatchAspectRatio(): boolean { + const targetRatio = this._params?.options.aspectRatio; + if (!targetRatio) { + return true; + } + const imageData = this._cropper!.getImageData(); + if (imageData.aspectRatio === targetRatio) { + return true; + } + + // If the image is not exactly the aspect ratio see if it is within a pixel. + if (imageData.naturalWidth > imageData.naturalHeight) { + const targetHeight = imageData.naturalWidth / targetRatio; + return Math.abs(targetHeight - imageData.naturalHeight) <= 1; + } + const targetWidth = imageData.naturalHeight * targetRatio; + return Math.abs(targetWidth - imageData.naturalWidth) <= 1; + } + protected render(): TemplateResult { return html` ${this.hass.localize("ui.common.cancel")} + ${this._isTargetAspectRatio + ? html` + ${this.hass.localize("ui.dialogs.image_cropper.use_original")} + ` + : nothing} + ${this.hass.localize("ui.dialogs.image_cropper.crop")} @@ -95,6 +124,11 @@ export class HaImagecropperDialog extends LitElement { ); } + private _useOriginal() { + this._params!.croppedCallback(this._params!.file); + this.closeDialog(); + } + static get styles(): CSSResultGroup { return [ haStyleDialog, diff --git a/src/translations/en.json b/src/translations/en.json index 3643651b0bae..5644d46b5c1d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1225,7 +1225,8 @@ }, "image_cropper": { "crop": "Crop", - "crop_image": "Picture to crop" + "crop_image": "Picture to crop", + "use_original": "Use original" }, "date-picker": { "today": "Today", From c4a69979147ce9ff1697a6ec9e5dede95484cf36 Mon Sep 17 00:00:00 2001 From: karwosts Date: Tue, 17 Dec 2024 16:27:55 -0800 Subject: [PATCH 3/7] move link into secondary --- src/components/ha-picture-upload.ts | 30 +++++++++++++++---- .../ha-selector/ha-selector-image.ts | 1 + .../areas/dialog-area-registry-detail.ts | 1 + .../config/person/dialog-person-detail.ts | 1 + src/translations/en.json | 3 +- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index a1499fc73271..1e14972287de 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -31,6 +31,9 @@ export class HaPictureUpload extends LitElement { @property({ type: Boolean }) public crop = false; + @property({ type: Boolean, attribute: "select-media" }) public selectMedia = + false; + @property({ attribute: false }) public cropOptions?: CropOptions; @property({ type: Boolean }) public original = false; @@ -39,15 +42,35 @@ export class HaPictureUpload extends LitElement { @state() private _uploading = false; + constructor() { + super(); + this._chooseMedia = this._chooseMedia.bind(this); + } + public render(): TemplateResult { if (!this.value) { + const secondary = + this.secondary || + (this.selectMedia + ? html`${this.hass.localize( + "ui.components.picture-upload.secondary", + { + select_media: html`${this.hass.localize( + "ui.components.picture-upload.select_media" + )}`, + } + )}` + : undefined); + return html` - ${this.hass.localize( - "ui.components.picture-upload.select_previous_upload" - )} `; } return html`
diff --git a/src/components/ha-selector/ha-selector-image.ts b/src/components/ha-selector/ha-selector-image.ts index 382281e3774a..5e01b04c85f7 100644 --- a/src/components/ha-selector/ha-selector-image.ts +++ b/src/components/ha-selector/ha-selector-image.ts @@ -96,6 +96,7 @@ export class HaImageSelector extends LitElement { .value=${this.value?.startsWith(URL_PREFIX) ? this.value : null} .original=${this.selector.image?.original} .cropOptions=${this.selector.image?.crop} + select-media @change=${this._pictureChanged} > `} diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index d8375babc442..59dcedc6f272 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -140,6 +140,7 @@ class DialogAreaDetail extends LitElement { .hass=${this.hass} .value=${this._picture} crop + select-media .cropOptions=${cropOptions} @change=${this._pictureChanged} > diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index e8199853cdce..c1a95e0f2f4d 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -137,6 +137,7 @@ class DialogPersonDetail extends LitElement { .hass=${this.hass} .value=${this._picture} crop + select-media .cropOptions=${cropOptions} @change=${this._pictureChanged} > diff --git a/src/translations/en.json b/src/translations/en.json index 5644d46b5c1d..864797b21d61 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -758,7 +758,8 @@ "current_image_alt": "Current picture", "supported_formats": "Supports JPEG, PNG, or GIF image.", "unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image.", - "select_previous_upload": "Select previous upload" + "secondary": "Drop your file here or {select_media}", + "select_media": "select from media" }, "color-picker": { "default": "default", From 5789587c4fc23a0fe2c7530b717dc22d6f477d99 Mon Sep 17 00:00:00 2001 From: karwosts Date: Thu, 19 Dec 2024 07:37:31 -0800 Subject: [PATCH 4/7] updates --- src/components/ha-picture-upload.ts | 30 ++++++++++--------- src/data/image_upload.ts | 4 +-- .../image-cropper-dialog.ts | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index 644c3d23fa46..ca2b20b00a3d 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -5,7 +5,12 @@ import { customElement, property, state } from "lit/decorators"; import type { MediaPickedEvent } from "../data/media-player"; import { fireEvent } from "../common/dom/fire_event"; import { haStyle } from "../resources/styles"; -import { createImage, generateImageThumbnailUrl } from "../data/image_upload"; +import { + MEDIA_PREFIX, + getIdFromUrl, + createImage, + generateImageThumbnailUrl, +} from "../data/image_upload"; import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { showImageCropperDialog } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; @@ -42,20 +47,18 @@ export class HaPictureUpload extends LitElement { @state() private _uploading = false; - constructor() { - super(); - this._chooseMedia = this._chooseMedia.bind(this); - } - public render(): TemplateResult { if (!this.value) { + /* eslint-disable lit-a11y/anchor-is-valid */ const secondary = this.secondary || (this.selectMedia ? html`${this.hass.localize( "ui.components.picture-upload.secondary", { - select_media: html`${this.hass.localize( "ui.components.picture-upload.select_media" )} { showMediaBrowserDialog(this, { action: "pick", entityId: "browser", navigateIds: [ { media_content_id: undefined, media_content_type: undefined }, { - media_content_id: "media-source://image_upload", + media_content_id: MEDIA_PREFIX, media_content_type: "app", }, ], minimumNavigateLevel: 2, mediaPickedCallback: async (pickedMedia: MediaPickedEvent) => { - const id = pickedMedia.item.media_content_id; - const stringToRemove = "media-source://image_upload/"; - if (id.startsWith(stringToRemove)) { - const mediaId = id.substr(stringToRemove.length); + const mediaId = getIdFromUrl(pickedMedia.item.media_content_id); + if (mediaId) { if (this.crop) { const url = generateImageThumbnailUrl(mediaId, undefined, true); const response = await fetch(url); @@ -212,7 +214,7 @@ export class HaPictureUpload extends LitElement { } }, }); - } + }; static get styles() { return [ diff --git a/src/data/image_upload.ts b/src/data/image_upload.ts index 504f7fb06031..dbf591a62ec4 100644 --- a/src/data/image_upload.ts +++ b/src/data/image_upload.ts @@ -9,7 +9,7 @@ interface Image { } export const URL_PREFIX = "/api/image/serve/"; -export const MEDIA_PREFIX = "media-source://image_upload/"; +export const MEDIA_PREFIX = "media-source://image_upload"; export interface ImageMutableParams { name: string; @@ -24,7 +24,7 @@ export const getIdFromUrl = (url: string): string | undefined => { id = id.substring(0, idx); } } else if (url.startsWith(MEDIA_PREFIX)) { - id = url.substring(MEDIA_PREFIX.length); + id = url.substring(MEDIA_PREFIX.length + 1); } return id; }; diff --git a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts index 280d2c319d41..57d6717cf07a 100644 --- a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts +++ b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts @@ -96,7 +96,7 @@ export class HaImagecropperDialog extends LitElement { ${this.hass.localize("ui.common.cancel")} ${this._isTargetAspectRatio - ? html` + ? html` ${this.hass.localize("ui.dialogs.image_cropper.use_original")} ` : nothing} From d87bf9a4ecbeccdac1741d8649a04eff8b613816 Mon Sep 17 00:00:00 2001 From: karwosts Date: Thu, 19 Dec 2024 10:43:46 -0800 Subject: [PATCH 5/7] use a button instead of a link --- src/components/ha-file-upload.ts | 9 +++++++++ src/components/ha-picture-upload.ts | 13 ++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/ha-file-upload.ts b/src/components/ha-file-upload.ts index f68e52f673a3..3c8eb6c775fd 100644 --- a/src/components/ha-file-upload.ts +++ b/src/components/ha-file-upload.ts @@ -320,6 +320,15 @@ export class HaFileUpload extends LitElement { .progress { color: var(--secondary-text-color); } + button.link { + background: none; + border: none; + padding: 0; + font-size: 14px; + color: var(--primary-color); + text-decoration: underline; + cursor: pointer; + } `; } } diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index ca2b20b00a3d..e1e9c76b150c 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -49,24 +49,23 @@ export class HaPictureUpload extends LitElement { public render(): TemplateResult { if (!this.value) { - /* eslint-disable lit-a11y/anchor-is-valid */ const secondary = this.secondary || (this.selectMedia ? html`${this.hass.localize( "ui.components.picture-upload.secondary", { - select_media: html`${this.hass.localize( + > + ${this.hass.localize( "ui.components.picture-upload.select_media" - )}`, + )} + `, } )}` : undefined); - /* eslint-enable lit-a11y/anchor-is-valid */ return html` Date: Thu, 19 Dec 2024 11:11:31 -0800 Subject: [PATCH 6/7] catch fetch exception --- src/components/ha-picture-upload.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index e1e9c76b150c..82d3988de7d1 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -195,8 +195,16 @@ export class HaPictureUpload extends LitElement { if (mediaId) { if (this.crop) { const url = generateImageThumbnailUrl(mediaId, undefined, true); - const response = await fetch(url); - const data = await response.blob(); + let data; + try { + const response = await fetch(url); + data = await response.blob(); + } catch (err: any) { + showAlertDialog(this, { + text: err.toString(), + }); + return; + } const metadata = { type: pickedMedia.item.media_content_type, }; From e10e856897085f1ab780c350c99d0af131f35413 Mon Sep 17 00:00:00 2001 From: karwosts Date: Thu, 19 Dec 2024 11:37:25 -0800 Subject: [PATCH 7/7] revert some no longer needed CSS --- src/components/ha-picture-upload.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts index 82d3988de7d1..9c2ee334bcb2 100644 --- a/src/components/ha-picture-upload.ts +++ b/src/components/ha-picture-upload.ts @@ -229,13 +229,10 @@ export class HaPictureUpload extends LitElement { css` :host { display: block; - } - ha-file-upload { height: 240px; } - ha-button.center { - display: flex; - align-items: center; + ha-file-upload { + height: 100%; } .center-vertical { display: flex;