mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-20 07:46:37 +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 {
|
.progress {
|
||||||
color: var(--secondary-text-color);
|
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 type { TemplateResult } from "lit";
|
||||||
import { LitElement, css, html } from "lit";
|
import { LitElement, css, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import type { MediaPickedEvent } from "../data/media-player";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { haStyle } from "../resources/styles";
|
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 { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||||
import { showImageCropperDialog } 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-button";
|
||||||
import "./ha-circular-progress";
|
import "./ha-circular-progress";
|
||||||
import "./ha-file-upload";
|
import "./ha-file-upload";
|
||||||
|
import { showMediaBrowserDialog } from "./media-player/show-media-browser-dialog";
|
||||||
|
|
||||||
@customElement("ha-picture-upload")
|
@customElement("ha-picture-upload")
|
||||||
export class HaPictureUpload extends LitElement {
|
export class HaPictureUpload extends LitElement {
|
||||||
@ -29,6 +36,9 @@ export class HaPictureUpload extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public crop = false;
|
@property({ type: Boolean }) public crop = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "select-media" }) public selectMedia =
|
||||||
|
false;
|
||||||
|
|
||||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||||
|
|
||||||
@property({ type: Boolean }) public original = false;
|
@property({ type: Boolean }) public original = false;
|
||||||
@ -39,13 +49,31 @@ export class HaPictureUpload extends LitElement {
|
|||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
if (!this.value) {
|
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`
|
return html`
|
||||||
<ha-file-upload
|
<ha-file-upload
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.icon=${mdiImagePlus}
|
.icon=${mdiImagePlus}
|
||||||
.label=${this.label ||
|
.label=${this.label ||
|
||||||
this.hass.localize("ui.components.picture-upload.label")}
|
this.hass.localize("ui.components.picture-upload.label")}
|
||||||
.secondary=${this.secondary}
|
.secondary=${secondary}
|
||||||
.supports=${this.supports ||
|
.supports=${this.supports ||
|
||||||
this.hass.localize("ui.components.picture-upload.supported_formats")}
|
this.hass.localize("ui.components.picture-upload.supported_formats")}
|
||||||
.uploading=${this._uploading}
|
.uploading=${this._uploading}
|
||||||
@ -93,7 +121,7 @@ export class HaPictureUpload extends LitElement {
|
|||||||
this.value = null;
|
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)) {
|
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
@ -109,7 +137,16 @@ export class HaPictureUpload extends LitElement {
|
|||||||
aspectRatio: NaN,
|
aspectRatio: NaN,
|
||||||
},
|
},
|
||||||
croppedCallback: (croppedFile) => {
|
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,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() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
@ -96,6 +96,7 @@ export class HaImageSelector extends LitElement {
|
|||||||
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
|
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
|
||||||
.original=${this.selector.image?.original}
|
.original=${this.selector.image?.original}
|
||||||
.cropOptions=${this.selector.image?.crop}
|
.cropOptions=${this.selector.image?.crop}
|
||||||
|
select-media
|
||||||
@change=${this._pictureChanged}
|
@change=${this._pictureChanged}
|
||||||
></ha-picture-upload>
|
></ha-picture-upload>
|
||||||
`}
|
`}
|
||||||
|
@ -85,7 +85,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
@opened=${this._dialogOpened}
|
@opened=${this._dialogOpened}
|
||||||
>
|
>
|
||||||
<ha-dialog-header show-border slot="heading">
|
<ha-dialog-header show-border slot="heading">
|
||||||
${this._navigateIds.length > 1
|
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
|
||||||
? html`
|
? html`
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
|
@ -10,6 +10,7 @@ export interface MediaPlayerBrowseDialogParams {
|
|||||||
entityId: string;
|
entityId: string;
|
||||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||||
navigateIds?: MediaPlayerItemId[];
|
navigateIds?: MediaPlayerItemId[];
|
||||||
|
minimumNavigateLevel?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showMediaBrowserDialog = (
|
export const showMediaBrowserDialog = (
|
||||||
|
@ -9,7 +9,7 @@ interface Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const URL_PREFIX = "/api/image/serve/";
|
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 {
|
export interface ImageMutableParams {
|
||||||
name: string;
|
name: string;
|
||||||
@ -24,7 +24,7 @@ export const getIdFromUrl = (url: string): string | undefined => {
|
|||||||
id = id.substring(0, idx);
|
id = id.substring(0, idx);
|
||||||
}
|
}
|
||||||
} else if (url.startsWith(MEDIA_PREFIX)) {
|
} else if (url.startsWith(MEDIA_PREFIX)) {
|
||||||
id = url.substring(MEDIA_PREFIX.length);
|
id = url.substring(MEDIA_PREFIX.length + 1);
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import Cropper from "cropperjs";
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import cropperCss from "cropperjs/dist/cropper.css";
|
import cropperCss from "cropperjs/dist/cropper.css";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
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 { customElement, property, state, query } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import "../../components/ha-dialog";
|
import "../../components/ha-dialog";
|
||||||
@ -23,6 +23,8 @@ export class HaImagecropperDialog extends LitElement {
|
|||||||
|
|
||||||
private _cropper?: Cropper;
|
private _cropper?: Cropper;
|
||||||
|
|
||||||
|
@state() private _isTargetAspectRatio?: boolean;
|
||||||
|
|
||||||
public showDialog(params: HaImageCropperDialogParams): void {
|
public showDialog(params: HaImageCropperDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._open = true;
|
this._open = true;
|
||||||
@ -33,6 +35,7 @@ export class HaImagecropperDialog extends LitElement {
|
|||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._cropper?.destroy();
|
this._cropper?.destroy();
|
||||||
this._cropper = undefined;
|
this._cropper = undefined;
|
||||||
|
this._isTargetAspectRatio = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
@ -47,6 +50,7 @@ export class HaImagecropperDialog extends LitElement {
|
|||||||
dragMode: "move",
|
dragMode: "move",
|
||||||
minCropBoxWidth: 50,
|
minCropBoxWidth: 50,
|
||||||
ready: () => {
|
ready: () => {
|
||||||
|
this._isTargetAspectRatio = this._checkMatchAspectRatio();
|
||||||
URL.revokeObjectURL(this._image!.src);
|
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 {
|
protected render(): TemplateResult {
|
||||||
return html`<ha-dialog
|
return html`<ha-dialog
|
||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
@ -72,6 +95,12 @@ export class HaImagecropperDialog extends LitElement {
|
|||||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||||
${this.hass.localize("ui.common.cancel")}
|
${this.hass.localize("ui.common.cancel")}
|
||||||
</mwc-button>
|
</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}>
|
<mwc-button slot="primaryAction" @click=${this._cropImage}>
|
||||||
${this.hass.localize("ui.dialogs.image_cropper.crop")}
|
${this.hass.localize("ui.dialogs.image_cropper.crop")}
|
||||||
</mwc-button>
|
</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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
|
@ -140,6 +140,7 @@ class DialogAreaDetail extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._picture}
|
.value=${this._picture}
|
||||||
crop
|
crop
|
||||||
|
select-media
|
||||||
.cropOptions=${cropOptions}
|
.cropOptions=${cropOptions}
|
||||||
@change=${this._pictureChanged}
|
@change=${this._pictureChanged}
|
||||||
></ha-picture-upload>
|
></ha-picture-upload>
|
||||||
|
@ -153,6 +153,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._picture}
|
.value=${this._picture}
|
||||||
crop
|
crop
|
||||||
|
select-media
|
||||||
.cropOptions=${cropOptions}
|
.cropOptions=${cropOptions}
|
||||||
@change=${this._pictureChanged}
|
@change=${this._pictureChanged}
|
||||||
></ha-picture-upload>
|
></ha-picture-upload>
|
||||||
|
@ -757,7 +757,9 @@
|
|||||||
"change_picture": "Change picture",
|
"change_picture": "Change picture",
|
||||||
"current_image_alt": "Current picture",
|
"current_image_alt": "Current picture",
|
||||||
"supported_formats": "Supports JPEG, PNG, or GIF image.",
|
"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": {
|
"color-picker": {
|
||||||
"default": "default",
|
"default": "default",
|
||||||
@ -1226,7 +1228,8 @@
|
|||||||
},
|
},
|
||||||
"image_cropper": {
|
"image_cropper": {
|
||||||
"crop": "Crop",
|
"crop": "Crop",
|
||||||
"crop_image": "Picture to crop"
|
"crop_image": "Picture to crop",
|
||||||
|
"use_original": "Use original"
|
||||||
},
|
},
|
||||||
"date-picker": {
|
"date-picker": {
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user