mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-20 15:56:35 +00:00
Allow selecting previously uploaded image for picture upload (#23072)
This commit is contained in:
parent
3b52d3d302
commit
65860a3142
@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,15 @@ 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";
|
||||
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";
|
||||
@ -12,6 +18,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 +36,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,13 +49,31 @@ export class HaPictureUpload extends LitElement {
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.value) {
|
||||
const secondary =
|
||||
this.secondary ||
|
||||
(this.selectMedia
|
||||
? html`${this.hass.localize(
|
||||
"ui.components.picture-upload.secondary",
|
||||
{
|
||||
select_media: html`<button
|
||||
class="link"
|
||||
@click=${this._chooseMedia}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.components.picture-upload.select_media"
|
||||
)}
|
||||
</button>`,
|
||||
}
|
||||
)}`
|
||||
: 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 +121,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 +137,16 @@ export class HaPictureUpload extends LitElement {
|
||||
aspectRatio: NaN,
|
||||
},
|
||||
croppedCallback: (croppedFile) => {
|
||||
if (mediaId && croppedFile === file) {
|
||||
this.value = generateImageThumbnailUrl(
|
||||
mediaId,
|
||||
this.size,
|
||||
this.original
|
||||
);
|
||||
fireEvent(this, "change");
|
||||
} else {
|
||||
this._uploadFile(croppedFile);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -141,6 +178,51 @@ export class HaPictureUpload extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _chooseMedia = () => {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "pick",
|
||||
entityId: "browser",
|
||||
navigateIds: [
|
||||
{ media_content_id: undefined, media_content_type: undefined },
|
||||
{
|
||||
media_content_id: MEDIA_PREFIX,
|
||||
media_content_type: "app",
|
||||
},
|
||||
],
|
||||
minimumNavigateLevel: 2,
|
||||
mediaPickedCallback: async (pickedMedia: MediaPickedEvent) => {
|
||||
const mediaId = getIdFromUrl(pickedMedia.item.media_content_id);
|
||||
if (mediaId) {
|
||||
if (this.crop) {
|
||||
const url = generateImageThumbnailUrl(mediaId, undefined, true);
|
||||
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,
|
||||
};
|
||||
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,
|
||||
|
@ -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}
|
||||
></ha-picture-upload>
|
||||
`}
|
||||
|
@ -85,7 +85,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
@opened=${this._dialogOpened}
|
||||
>
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
${this._navigateIds.length > 1
|
||||
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
|
@ -10,6 +10,7 @@ export interface MediaPlayerBrowseDialogParams {
|
||||
entityId: string;
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||
navigateIds?: MediaPlayerItemId[];
|
||||
minimumNavigateLevel?: number;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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}>
|
||||
${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,
|
||||
|
@ -140,6 +140,7 @@ class DialogAreaDetail extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
crop
|
||||
select-media
|
||||
.cropOptions=${cropOptions}
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
|
@ -153,6 +153,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
crop
|
||||
select-media
|
||||
.cropOptions=${cropOptions}
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
|
@ -757,7 +757,9 @@
|
||||
"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.",
|
||||
"secondary": "Drop your file here or {select_media}",
|
||||
"select_media": "select from media"
|
||||
},
|
||||
"color-picker": {
|
||||
"default": "default",
|
||||
@ -1226,7 +1228,8 @@
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop": "Crop",
|
||||
"crop_image": "Picture to crop"
|
||||
"crop_image": "Picture to crop",
|
||||
"use_original": "Use original"
|
||||
},
|
||||
"date-picker": {
|
||||
"today": "Today",
|
||||
|
Loading…
x
Reference in New Issue
Block a user