Update upload element (#17654)

This commit is contained in:
Bram Kragten 2023-08-30 13:24:11 +02:00 committed by GitHub
parent d350c35c4e
commit 2ab67328d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 335 additions and 177 deletions

View File

@ -31,6 +31,7 @@ export class HassioUploadBackup extends LitElement {
.icon=${mdiFolderUpload} .icon=${mdiFolderUpload}
accept="application/x-tar" accept="application/x-tar"
label="Upload backup" label="Upload backup"
supports="Supports .TAR files"
@file-picked=${this._uploadFile} @file-picked=${this._uploadFile}
auto-open-file-dialog auto-open-file-dialog
></ha-file-upload> ></ha-file-upload>

View File

@ -1,16 +1,19 @@
import { styles } from "@material/mwc-textfield/mwc-textfield.css"; import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiClose } from "@mdi/js"; import { mdiDelete, mdiFileUpload } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-circular-progress"; import "./ha-button";
import "./ha-icon-button"; import "./ha-icon-button";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ensureArray } from "../common/array/ensure-array";
import { bytesToString } from "../util/bytes-to-string";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"file-picked": { files: FileList }; "file-picked": { files: File[] };
} }
} }
@ -22,12 +25,22 @@ export class HaFileUpload extends LitElement {
@property() public icon?: string; @property() public icon?: string;
@property() public label!: string; @property() public label?: string;
@property() public value: string | TemplateResult | null = null; @property() public secondary?: string;
@property() public supports?: string;
@property() public value?: File | File[] | FileList | string;
@property({ type: Boolean }) private multiple = false;
@property({ type: Boolean, reflect: true }) public disabled: boolean = false;
@property({ type: Boolean }) private uploading = false; @property({ type: Boolean }) private uploading = false;
@property({ type: Number }) private progress?: number;
@property({ type: Boolean, attribute: "auto-open-file-dialog" }) @property({ type: Boolean, attribute: "auto-open-file-dialog" })
private autoOpenFileDialog = false; private autoOpenFileDialog = false;
@ -45,72 +58,102 @@ export class HaFileUpload extends LitElement {
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
${this.uploading ${this.uploading
? html`<ha-circular-progress ? html`<div class="container">
alt="Uploading" <div class="row">
size="large" <span class="header"
active >${this.value
></ha-circular-progress>` ? this.hass?.localize(
: html` "ui.components.file-upload.uploading_name",
<label { name: this.value }
for="input" )
class="mdc-text-field mdc-text-field--filled ${classMap({ : this.hass?.localize(
"mdc-text-field--focused": this._drag, "ui.components.file-upload.uploading"
"mdc-text-field--with-leading-icon": Boolean(this.icon), )}</span
"mdc-text-field--with-trailing-icon": Boolean(this.value),
})}"
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
>
<span class="mdc-text-field__ripple"></span>
<span
class="mdc-floating-label ${this.value || this._drag
? "mdc-floating-label--float-above"
: ""}"
id="label"
>${this.label}</span
> >
${this.icon ${this.progress
? html`<span ? html`<span class="progress"
class="mdc-text-field__icon mdc-text-field__icon--leading" >${this.progress}${blankBeforePercent(
> this.hass!.locale
<ha-icon-button )}%</span
@click=${this._openFilePicker} >`
.path=${this.icon}
></ha-icon-button>
</span>`
: ""} : ""}
<div class="value">${this.value}</div> </div>
<input <mwc-linear-progress
id="input" .indeterminate=${!this.progress}
type="file" .progress=${this.progress ? this.progress / 100 : undefined}
class="mdc-text-field__input file" ></mwc-linear-progress>
accept=${this.accept} </div>`
@change=${this._handleFilePicked} : html`<label
aria-labelledby="label" for=${this.value ? "" : "input"}
/> class="container ${classMap({
${this.value dragged: this._drag,
? html`<span multiple: this.multiple,
class="mdc-text-field__icon mdc-text-field__icon--trailing" value: Boolean(this.value),
})}"
@drop=${this._handleDrop}
@dragenter=${this._handleDragStart}
@dragover=${this._handleDragStart}
@dragleave=${this._handleDragEnd}
@dragend=${this._handleDragEnd}
>${!this.value
? html`<ha-svg-icon
class="big-icon"
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
<ha-button unelevated @click=${this._openFilePicker}>
${this.label ||
this.hass?.localize("ui.components.file-upload.label")}
</ha-button>
<span class="secondary"
>${this.secondary ||
this.hass?.localize(
"ui.components.file-upload.secondary"
)}</span
> >
<ha-icon-button <span class="supports">${this.supports}</span>`
slot="suffix" : typeof this.value === "string"
@click=${this._clearValue} ? html`<div class="row">
.label=${this.hass?.localize("ui.common.close") || <div class="value" @click=${this._openFilePicker}>
"close"} <ha-svg-icon
.path=${mdiClose} .path=${this.icon || mdiFileUpload}
></ha-icon-button> ></ha-svg-icon>
</span>` ${this.value}
: ""} </div>
<span <ha-icon-button
class="mdc-line-ripple ${this._drag @click=${this._clearValue}
? "mdc-line-ripple--active" .label=${this.hass?.localize("ui.common.delete") ||
: ""}" "Delete"}
></span> .path=${mdiDelete}
</label> ></ha-icon-button>
`} </div>`
: (this.value instanceof FileList
? Array.from(this.value)
: ensureArray(this.value)
).map(
(file) =>
html`<div class="row">
<div class="value" @click=${this._openFilePicker}>
<ha-svg-icon
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
${file.name} - ${bytesToString(file.size)}
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.path=${mdiDelete}
></ha-icon-button>
</div>`
)}
<input
id="input"
type="file"
class="file"
.accept=${this.accept}
.multiple=${this.multiple}
@change=${this._handleFilePicked}
/></label>`}
`; `;
} }
@ -122,7 +165,12 @@ export class HaFileUpload extends LitElement {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
if (ev.dataTransfer?.files) { if (ev.dataTransfer?.files) {
fireEvent(this, "file-picked", { files: ev.dataTransfer.files }); fireEvent(this, "file-picked", {
files:
this.multiple || ev.dataTransfer.files.length === 1
? Array.from(ev.dataTransfer.files)
: [ev.dataTransfer.files[0]],
});
} }
this._drag = false; this._drag = false;
} }
@ -140,93 +188,121 @@ export class HaFileUpload extends LitElement {
} }
private _handleFilePicked(ev) { private _handleFilePicked(ev) {
if (ev.target.files.length === 0) {
return;
}
this.value = ev.target.files;
fireEvent(this, "file-picked", { files: ev.target.files }); fireEvent(this, "file-picked", { files: ev.target.files });
} }
private _clearValue(ev: Event) { private _clearValue(ev: Event) {
ev.preventDefault(); ev.preventDefault();
this.value = null;
this._input!.value = ""; this._input!.value = "";
this.value = undefined;
fireEvent(this, "change"); fireEvent(this, "change");
} }
static get styles() { static get styles() {
return [ return css`
styles, :host {
css` display: block;
:host { height: 240px;
display: block; }
} :host([disabled]) {
.mdc-text-field--filled { pointer-events: none;
height: auto; color: var(--disabled-text-color);
padding-top: 16px; }
cursor: pointer; .container {
} position: relative;
.mdc-text-field--filled.mdc-text-field--with-trailing-icon { display: flex;
padding-top: 28px; flex-direction: column;
} justify-content: center;
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon { align-items: center;
color: var(--secondary-text-color); border: solid 1px
} var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
.mdc-text-field--filled.mdc-text-field--with-trailing-icon border-radius: var(--mdc-shape-small, 4px);
.mdc-text-field__icon { height: 100%;
align-self: flex-end; }
} label.container {
.mdc-text-field__icon--leading { border: dashed 1px
margin-bottom: 12px; var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
inset-inline-start: initial; cursor: pointer;
inset-inline-end: 0px; }
direction: var(--direction); :host([disabled]) .container {
} border-color: var(--disabled-color);
.mdc-text-field--filled .mdc-floating-label--float-above { }
transform: scale(0.75); label.dragged {
top: 8px; border-color: var(--primary-color);
} }
.mdc-floating-label { .dragged:before {
inset-inline-start: 16px !important; position: absolute;
inset-inline-end: initial !important; top: 0;
direction: var(--direction); right: 0;
} bottom: 0;
.mdc-text-field--filled .mdc-floating-label { left: 0;
inset-inline-start: 48px !important; background-color: var(--primary-color);
inset-inline-end: initial !important; content: "";
direction: var(--direction); opacity: var(--dark-divider-opacity);
} pointer-events: none;
.mdc-text-field__icon--trailing { border-radius: var(--mdc-shape-small, 4px);
pointer-events: auto !important; }
} label.value {
.dragged:before { cursor: default;
position: var(--layout-fit_-_position); }
top: var(--layout-fit_-_top); label.value.multiple {
right: var(--layout-fit_-_right); justify-content: unset;
bottom: var(--layout-fit_-_bottom); overflow: auto;
left: var(--layout-fit_-_left); }
background: currentColor; .highlight {
content: ""; color: var(--primary-color);
opacity: var(--dark-divider-opacity); }
pointer-events: none; .row {
border-radius: 4px; display: flex;
} width: 100%;
.value { align-items: center;
width: 100%; justify-content: space-between;
} padding: 0 16px;
input.file { box-sizing: border-box;
display: none; }
} ha-button {
img { margin-bottom: 4px;
max-width: 100%; }
max-height: 125px; .supports {
} color: var(--secondary-text-color);
ha-icon-button { font-size: 12px;
--mdc-icon-button-size: 24px; }
--mdc-icon-size: 20px; :host([disabled]) .secondary {
} color: var(--disabled-text-color);
ha-circular-progress { }
display: block; input.file {
text-align-last: center; display: none;
} }
`, .value {
]; cursor: pointer;
}
.value ha-svg-icon {
margin-right: 8px;
}
.big-icon {
--mdc-icon-size: 48px;
margin-bottom: 8px;
}
ha-button {
--mdc-button-outline-color: var(--primary-color);
--mdc-icon-button-size: 24px;
}
mwc-linear-progress {
width: 100%;
padding: 16px;
box-sizing: border-box;
}
.header {
font-weight: 500;
}
.progress {
color: var(--secondary-text-color);
}
`;
} }
} }

View File

@ -1,5 +1,5 @@
import { mdiImagePlus } from "@mdi/js"; import { mdiImagePlus } from "@mdi/js";
import { html, LitElement, TemplateResult } from "lit"; import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload"; import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
@ -9,6 +9,7 @@ import {
showImageCropperDialog, showImageCropperDialog,
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-circular-progress"; import "./ha-circular-progress";
import "./ha-file-upload"; import "./ha-file-upload";
@ -20,6 +21,12 @@ export class HaPictureUpload extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public secondary?: string;
@property() public supports?: string;
@property() public currentImageAltText?: string;
@property({ type: Boolean }) public crop = false; @property({ type: Boolean }) public crop = false;
@property({ attribute: false }) public cropOptions?: CropOptions; @property({ attribute: false }) public cropOptions?: CropOptions;
@ -29,19 +36,44 @@ export class HaPictureUpload extends LitElement {
@state() private _uploading = false; @state() private _uploading = false;
public render(): TemplateResult { public render(): TemplateResult {
return html` if (!this.value) {
<ha-file-upload return html`
.hass=${this.hass} <ha-file-upload
.icon=${mdiImagePlus} .hass=${this.hass}
.label=${this.label || .icon=${mdiImagePlus}
this.hass.localize("ui.components.picture-upload.label")} .label=${this.label ||
.uploading=${this._uploading} this.hass.localize("ui.components.picture-upload.label")}
.value=${this.value ? html`<img .src=${this.value} />` : ""} .secondary=${this.secondary}
@file-picked=${this._handleFilePicked} .supports=${this.supports ||
@change=${this._handleFileCleared} this.hass.localize("ui.components.picture-upload.supported_formats")}
accept="image/png, image/jpeg, image/gif" .uploading=${this._uploading}
></ha-file-upload> @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")}
/>
<ha-button
@click=${this._handleChangeClick}
.label=${this.hass.localize(
"ui.components.picture-upload.change_picture"
)}
>
</ha-button>
</div>
</div>`;
}
private _handleChangeClick() {
this.value = null;
fireEvent(this, "change");
} }
private async _handleFilePicked(ev) { private async _handleFilePicked(ev) {
@ -100,6 +132,35 @@ export class HaPictureUpload extends LitElement {
this._uploading = false; this._uploading = false;
} }
} }
static get styles() {
return 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);
}
`;
}
} }
declare global { declare global {

View File

@ -37,9 +37,12 @@ export class HaFileSelector extends LitElement {
.label=${this.label} .label=${this.label}
.required=${this.required} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .supports=${this.helper}
.uploading=${this._busy} .uploading=${this._busy}
.value=${this.value ? this._filename?.name || "Unknown file" : ""} .value=${this.value
? this._filename?.name ||
this.hass.localize("ui.components.selectors.file.unknown_file")
: undefined}
@file-picked=${this._uploadFile} @file-picked=${this._uploadFile}
@change=${this._removeFile} @change=${this._removeFile}
></ha-file-upload> ></ha-file-upload>

View File

@ -100,6 +100,14 @@ class DialogAreaDetail extends LitElement {
dialogInitialFocus dialogInitialFocus
></ha-textfield> ></ha-textfield>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
crop
.cropOptions=${cropOptions}
@change=${this._pictureChanged}
></ha-picture-upload>
<div class="label"> <div class="label">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.areas.editor.aliases_section" "ui.panel.config.areas.editor.aliases_section"
@ -132,14 +140,6 @@ class DialogAreaDetail extends LitElement {
"ui.panel.config.areas.editor.aliases_description" "ui.panel.config.areas.editor.aliases_description"
)} )}
</div> </div>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
crop
.cropOptions=${cropOptions}
@change=${this._pictureChanged}
></ha-picture-upload>
</div> </div>
</div> </div>
${entry ${entry
@ -229,7 +229,8 @@ class DialogAreaDetail extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-textfield { ha-textfield,
ha-picture-upload {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }

View File

@ -110,10 +110,10 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.uploading=${this._uploading} .uploading=${this._uploading}
.icon=${mdiFileUpload} .icon=${mdiFileUpload}
label=${this._firmwareFile?.name ?? .label=${this.hass.localize(
this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.upload_firmware" "ui.panel.config.zwave_js.update_firmware.upload_firmware"
)} )}
.value=${this._firmwareFile}
@file-picked=${this._uploadFile} @file-picked=${this._uploadFile}
></ha-file-upload> ></ha-file-upload>
${this._nodeStatus.is_controller_node ${this._nodeStatus.is_controller_node

View File

@ -126,6 +126,7 @@ class DialogPersonDetail extends LitElement {
)} )}
required required
></ha-textfield> ></ha-textfield>
<ha-picture-upload <ha-picture-upload
.hass=${this.hass} .hass=${this.hass}
.value=${this._picture} .value=${this._picture}
@ -422,7 +423,8 @@ class DialogPersonDetail extends LitElement {
display: block; display: block;
} }
ha-picture-upload { ha-picture-upload {
margin-top: 16px; margin-bottom: 16px;
--file-upload-image-border-radius: 50%;
} }
ha-formfield { ha-formfield {
display: block; display: block;

View File

@ -318,6 +318,10 @@
"manual": "Manually enter media ID", "manual": "Manually enter media ID",
"media_content_id": "Media content ID", "media_content_id": "Media content ID",
"media_content_type": "Media content type" "media_content_type": "Media content type"
},
"file": {
"upload_failed": "Upload failed",
"unknown_file": "Unknown file"
} }
}, },
"logbook": { "logbook": {
@ -498,8 +502,18 @@
"filtered_by_device": "device: {device_name}", "filtered_by_device": "device: {device_name}",
"filtered_by_area": "area: {area_name}" "filtered_by_area": "area: {area_name}"
}, },
"file-upload": {
"uploading": "Uploading...",
"uploading_name": "Uploading {name}",
"label": "Add file",
"secondary": "Or drop your file here",
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image."
},
"picture-upload": { "picture-upload": {
"label": "Picture", "label": "Add picture",
"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."
}, },
"date-range-picker": { "date-range-picker": {