-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow selecting previously uploaded image for picture upload #23072
Changes from 3 commits
1ce314d
a201faa
c4a6997
de5d995
5789587
d87bf9a
95156b8
e10e856
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 { | ||||||
|
@@ -29,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; | ||||||
|
@@ -37,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`<a href="#" @click=${this._chooseMedia} | ||||||
>${this.hass.localize( | ||||||
"ui.components.picture-upload.select_media" | ||||||
)}</a | ||||||
>`, | ||||||
} | ||||||
)}` | ||||||
: undefined); | ||||||
|
||||||
return html` | ||||||
<ha-file-upload | ||||||
.hass=${this.hass} | ||||||
.icon=${mdiImagePlus} | ||||||
.label=${this.label || | ||||||
this.hass.localize("ui.components.picture-upload.label")} | ||||||
.secondary=${this.secondary} | ||||||
.secondary=${secondary} | ||||||
.supports=${this.supports || | ||||||
this.hass.localize("ui.components.picture-upload.supported_formats")} | ||||||
.uploading=${this._uploading} | ||||||
|
@@ -93,7 +118,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( | ||||||
|
@@ -109,7 +134,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); | ||||||
} | ||||||
}, | ||||||
}); | ||||||
} | ||||||
|
@@ -141,16 +175,58 @@ export class HaPictureUpload extends LitElement { | |||||
} | ||||||
} | ||||||
|
||||||
private _chooseMedia(): void { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
showMediaBrowserDialog(this, { | ||||||
action: "pick", | ||||||
entityId: "browser", | ||||||
navigateIds: [ | ||||||
{ media_content_id: undefined, media_content_type: undefined }, | ||||||
{ | ||||||
media_content_id: "media-source://image_upload", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add a static constant for |
||||||
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); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please use |
||||||
if (this.crop) { | ||||||
const url = generateImageThumbnailUrl(mediaId, undefined, true); | ||||||
const response = await fetch(url); | ||||||
const data = await response.blob(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can fail, please add error handling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate a bit what you want to see here? So HA gives us the list of images, we click on one, and then... it's not available? What should I do with that, a popup dialog that just says "unknown error"? I would have thought just whatever default logging exception to the console would be sufficient, and how this would usually be handled. Or what is the specific mode of failure you were thinking of? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. If it fails to load use I know this is an edge case but http requests can always fail and showing it only in console won't help the user in the situation. |
||||||
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"); | ||||||
} | ||||||
} | ||||||
}, | ||||||
}); | ||||||
} | ||||||
|
||||||
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; | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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`<ha-dialog | ||||||
@closed=${this.closeDialog} | ||||||
|
@@ -72,6 +95,12 @@ export class HaImagecropperDialog extends LitElement { | |||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}> | ||||||
${this.hass.localize("ui.common.cancel")} | ||||||
</mwc-button> | ||||||
${this._isTargetAspectRatio | ||||||
? html` <mwc-button slot="primaryAction" @click=${this._useOriginal}> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
${this.hass.localize("ui.dialogs.image_cropper.use_original")} | ||||||
</mwc-button>` | ||||||
: nothing} | ||||||
|
||||||
<mwc-button slot="primaryAction" @click=${this._cropImage}> | ||||||
${this.hass.localize("ui.dialogs.image_cropper.crop")} | ||||||
</mwc-button> | ||||||
|
@@ -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, | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use arrow function instead