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 { MEDIA_PREFIX, getIdFromUrl, createImage, generateImageThumbnailUrl, getImageData, } 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"; import type { HomeAssistant } from "../types"; import "./ha-button"; import "./ha-file-upload"; import { showMediaBrowserDialog } from "./media-player/show-media-browser-dialog"; @customElement("ha-picture-upload") export class HaPictureUpload extends LitElement { public hass!: HomeAssistant; @property() public value: string | null = null; @property() public label?: string; @property() public secondary?: string; @property() public supports?: string; @property({ attribute: false }) public currentImageAltText?: string; @property({ type: Boolean }) public crop = false; @property({ type: Boolean, attribute: "select-media" }) public selectMedia = false; // This property is set when this component is used inside a media selector. // When set, it returns selected media or uploaded files as MediaSelectorValue // When unset, it only allows selecting images from image-upload, and returns // selected or uploaded images as a string starting with /api/... @property({ type: Boolean, attribute: "full-media" }) public fullMedia = false; @property({ attribute: false }) public contentIdHelper?: string; @property({ attribute: false }) public cropOptions?: CropOptions; @property({ type: Boolean }) public original = false; @property({ type: Number }) public size = 512; @state() private _uploading = false; public render(): TemplateResult { if (!this.value) { const secondary = this.secondary || (this.selectMedia ? html`${this.hass.localize( "ui.components.picture-upload.secondary", { select_media: html``, } )}` : undefined); return html` `; } return html`
${this.currentImageAltText
${this.hass.localize("ui.components.picture-upload.clear_picture")}
`; } private _handleChangeClick() { this.value = null; fireEvent(this, "change"); } private async _handleFilePicked(ev) { const file = ev.detail.files[0]; if (this.crop) { this._cropFile(file); } else { this._uploadFile(file); } } private async _handleFileCleared() { this.value = null; } private async _cropFile(file: File, mediaId?: string) { if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) { showAlertDialog(this, { text: this.hass.localize( "ui.components.picture-upload.unsupported_format" ), }); return; } showImageCropperDialog(this, { file, options: this.cropOptions || { round: false, aspectRatio: NaN, }, croppedCallback: (croppedFile) => { if (mediaId && croppedFile === file) { this.value = generateImageThumbnailUrl( mediaId, this.size, this.original ); fireEvent(this, "change"); } else { this._uploadFile(croppedFile); } }, }); } private async _uploadFile(file: File) { if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) { showAlertDialog(this, { text: this.hass.localize( "ui.components.picture-upload.unsupported_format" ), }); return; } this._uploading = true; try { const media = await createImage(this.hass, file); if (this.fullMedia) { const item = { media_content_id: `${MEDIA_PREFIX}/${media.id}`, media_content_type: media.content_type, title: media.name, media_class: "image" as const, can_play: true, can_expand: false, can_search: false, thumbnail: generateImageThumbnailUrl(media.id, 256), } as const; const navigateIds = [ {}, { media_content_type: "app", media_content_id: MEDIA_PREFIX }, ]; fireEvent(this, "media-picked", { item, navigateIds, }); } else { this.value = generateImageThumbnailUrl( media.id, this.size, this.original ); fireEvent(this, "change"); } } catch (err: any) { showAlertDialog(this, { text: err.toString(), }); } finally { this._uploading = false; } } private _chooseMedia = () => { showMediaBrowserDialog(this, { action: "pick", entityId: "browser", accept: ["image/*"], navigateIds: this.fullMedia ? undefined : [ { media_content_id: undefined, media_content_type: undefined }, { media_content_id: MEDIA_PREFIX, media_content_type: "app", }, ], minimumNavigateLevel: this.fullMedia ? undefined : 2, hideContentType: true, contentIdHelper: this.contentIdHelper, mediaPickedCallback: async (pickedMedia: MediaPickedEvent) => { if (this.fullMedia) { fireEvent(this, "media-picked", pickedMedia); return; } const mediaId = getIdFromUrl(pickedMedia.item.media_content_id); if (mediaId) { if (this.crop) { const url = generateImageThumbnailUrl(mediaId, undefined, true); let data; try { data = await getImageData(this.hass, url); } 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, 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); transition: opacity 0.3s; opacity: var(--picture-opacity, 1); } img:hover { opacity: 1; } `, ]; } } declare global { interface HTMLElementTagNameMap { "ha-picture-upload": HaPictureUpload; } }