mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-10 03:19:44 +00:00
308 lines
8.7 KiB
TypeScript
308 lines
8.7 KiB
TypeScript
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`<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=${secondary}
|
|
.supports=${this.supports ||
|
|
this.hass.localize("ui.components.picture-upload.supported_formats")}
|
|
.uploading=${this._uploading}
|
|
@file-picked=${this._handleFilePicked}
|
|
@change=${this._handleFileCleared}
|
|
accept="image/png, image/jpeg, image/gif"
|
|
></ha-file-upload>
|
|
`;
|
|
}
|
|
return html`<div class="center-vertical">
|
|
<div class="value">
|
|
<img
|
|
.src=${this.value}
|
|
alt=${this.currentImageAltText ||
|
|
this.hass.localize("ui.components.picture-upload.current_image_alt")}
|
|
/>
|
|
<div>
|
|
<ha-button
|
|
appearance="plain"
|
|
size="small"
|
|
variant="danger"
|
|
@click=${this._handleChangeClick}
|
|
>
|
|
${this.hass.localize("ui.components.picture-upload.clear_picture")}
|
|
</ha-button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|