diff --git a/hassio/src/components/hassio-upload-backup.ts b/hassio/src/components/hassio-upload-backup.ts
index d40345522e..57046f2818 100644
--- a/hassio/src/components/hassio-upload-backup.ts
+++ b/hassio/src/components/hassio-upload-backup.ts
@@ -31,6 +31,7 @@ export class HassioUploadBackup extends LitElement {
.icon=${mdiFolderUpload}
accept="application/x-tar"
label="Upload backup"
+ supports="Supports .TAR files"
@file-picked=${this._uploadFile}
auto-open-file-dialog
>
diff --git a/src/components/ha-file-upload.ts b/src/components/ha-file-upload.ts
index db6a029bd0..cc0e97a29c 100644
--- a/src/components/ha-file-upload.ts
+++ b/src/components/ha-file-upload.ts
@@ -1,16 +1,19 @@
-import { styles } from "@material/mwc-textfield/mwc-textfield.css";
-import { mdiClose } from "@mdi/js";
-import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
+import "@material/mwc-linear-progress/mwc-linear-progress";
+import { mdiDelete, mdiFileUpload } from "@mdi/js";
+import { LitElement, PropertyValues, TemplateResult, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
-import "./ha-circular-progress";
+import "./ha-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 {
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 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: Number }) private progress?: number;
+
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
private autoOpenFileDialog = false;
@@ -45,72 +58,102 @@ export class HaFileUpload extends LitElement {
public render(): TemplateResult {
return html`
${this.uploading
- ? html``
- : html`
- `}
`;
}
@@ -122,7 +165,12 @@ export class HaFileUpload extends LitElement {
ev.preventDefault();
ev.stopPropagation();
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;
}
@@ -140,93 +188,121 @@ export class HaFileUpload extends LitElement {
}
private _handleFilePicked(ev) {
+ if (ev.target.files.length === 0) {
+ return;
+ }
+ this.value = ev.target.files;
fireEvent(this, "file-picked", { files: ev.target.files });
}
private _clearValue(ev: Event) {
ev.preventDefault();
- this.value = null;
this._input!.value = "";
+ this.value = undefined;
fireEvent(this, "change");
}
static get styles() {
- return [
- styles,
- css`
- :host {
- display: block;
- }
- .mdc-text-field--filled {
- height: auto;
- padding-top: 16px;
- cursor: pointer;
- }
- .mdc-text-field--filled.mdc-text-field--with-trailing-icon {
- padding-top: 28px;
- }
- .mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
- color: var(--secondary-text-color);
- }
- .mdc-text-field--filled.mdc-text-field--with-trailing-icon
- .mdc-text-field__icon {
- align-self: flex-end;
- }
- .mdc-text-field__icon--leading {
- margin-bottom: 12px;
- inset-inline-start: initial;
- inset-inline-end: 0px;
- direction: var(--direction);
- }
- .mdc-text-field--filled .mdc-floating-label--float-above {
- transform: scale(0.75);
- top: 8px;
- }
- .mdc-floating-label {
- inset-inline-start: 16px !important;
- inset-inline-end: initial !important;
- direction: var(--direction);
- }
- .mdc-text-field--filled .mdc-floating-label {
- inset-inline-start: 48px !important;
- inset-inline-end: initial !important;
- direction: var(--direction);
- }
- .mdc-text-field__icon--trailing {
- pointer-events: auto !important;
- }
- .dragged:before {
- position: var(--layout-fit_-_position);
- top: var(--layout-fit_-_top);
- right: var(--layout-fit_-_right);
- bottom: var(--layout-fit_-_bottom);
- left: var(--layout-fit_-_left);
- background: currentColor;
- content: "";
- opacity: var(--dark-divider-opacity);
- pointer-events: none;
- border-radius: 4px;
- }
- .value {
- width: 100%;
- }
- input.file {
- display: none;
- }
- img {
- max-width: 100%;
- max-height: 125px;
- }
- ha-icon-button {
- --mdc-icon-button-size: 24px;
- --mdc-icon-size: 20px;
- }
- ha-circular-progress {
- display: block;
- text-align-last: center;
- }
- `,
- ];
+ return css`
+ :host {
+ display: block;
+ height: 240px;
+ }
+ :host([disabled]) {
+ pointer-events: none;
+ color: var(--disabled-text-color);
+ }
+ .container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ border: solid 1px
+ var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
+ border-radius: var(--mdc-shape-small, 4px);
+ height: 100%;
+ }
+ label.container {
+ border: dashed 1px
+ var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
+ cursor: pointer;
+ }
+ :host([disabled]) .container {
+ border-color: var(--disabled-color);
+ }
+ label.dragged {
+ border-color: var(--primary-color);
+ }
+ .dragged:before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: var(--primary-color);
+ content: "";
+ opacity: var(--dark-divider-opacity);
+ pointer-events: none;
+ border-radius: var(--mdc-shape-small, 4px);
+ }
+ label.value {
+ cursor: default;
+ }
+ label.value.multiple {
+ justify-content: unset;
+ overflow: auto;
+ }
+ .highlight {
+ color: var(--primary-color);
+ }
+ .row {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 16px;
+ box-sizing: border-box;
+ }
+ ha-button {
+ margin-bottom: 4px;
+ }
+ .supports {
+ color: var(--secondary-text-color);
+ font-size: 12px;
+ }
+ :host([disabled]) .secondary {
+ color: var(--disabled-text-color);
+ }
+ input.file {
+ 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);
+ }
+ `;
}
}
diff --git a/src/components/ha-picture-upload.ts b/src/components/ha-picture-upload.ts
index 0a21f82ec7..7a23a3b093 100644
--- a/src/components/ha-picture-upload.ts
+++ b/src/components/ha-picture-upload.ts
@@ -1,5 +1,5 @@
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 { fireEvent } from "../common/dom/fire_event";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
@@ -9,6 +9,7 @@ import {
showImageCropperDialog,
} from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { HomeAssistant } from "../types";
+import "./ha-button";
import "./ha-circular-progress";
import "./ha-file-upload";
@@ -20,6 +21,12 @@ export class HaPictureUpload extends LitElement {
@property() public label?: string;
+ @property() public secondary?: string;
+
+ @property() public supports?: string;
+
+ @property() public currentImageAltText?: string;
+
@property({ type: Boolean }) public crop = false;
@property({ attribute: false }) public cropOptions?: CropOptions;
@@ -29,19 +36,44 @@ export class HaPictureUpload extends LitElement {
@state() private _uploading = false;
public render(): TemplateResult {
- return html`
- ` : ""}
- @file-picked=${this._handleFilePicked}
- @change=${this._handleFileCleared}
- accept="image/png, image/jpeg, image/gif"
- >
- `;
+ if (!this.value) {
+ return html`
+
+ `;
+ }
+ return html`
+
+
![${this.currentImageAltText]()
+
+
+
+
`;
+ }
+
+ private _handleChangeClick() {
+ this.value = null;
+ fireEvent(this, "change");
}
private async _handleFilePicked(ev) {
@@ -100,6 +132,35 @@ export class HaPictureUpload extends LitElement {
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 {
diff --git a/src/components/ha-selector/ha-selector-file.ts b/src/components/ha-selector/ha-selector-file.ts
index f46ca12696..2024d04b54 100644
--- a/src/components/ha-selector/ha-selector-file.ts
+++ b/src/components/ha-selector/ha-selector-file.ts
@@ -37,9 +37,12 @@ export class HaFileSelector extends LitElement {
.label=${this.label}
.required=${this.required}
.disabled=${this.disabled}
- .helper=${this.helper}
+ .supports=${this.helper}
.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}
@change=${this._removeFile}
>
diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts
index 4b2d913413..9a1bd141e6 100644
--- a/src/panels/config/areas/dialog-area-registry-detail.ts
+++ b/src/panels/config/areas/dialog-area-registry-detail.ts
@@ -100,6 +100,14 @@ class DialogAreaDetail extends LitElement {
dialogInitialFocus
>
+
+
${this.hass.localize(
"ui.panel.config.areas.editor.aliases_section"
@@ -132,14 +140,6 @@ class DialogAreaDetail extends LitElement {
"ui.panel.config.areas.editor.aliases_description"
)}
-
-
${entry
@@ -229,7 +229,8 @@ class DialogAreaDetail extends LitElement {
return [
haStyleDialog,
css`
- ha-textfield {
+ ha-textfield,
+ ha-picture-upload {
display: block;
margin-bottom: 16px;
}
diff --git a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-update-firmware-node.ts b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-update-firmware-node.ts
index 68d0ff8235..b154162f83 100644
--- a/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-update-firmware-node.ts
+++ b/src/panels/config/integrations/integration-panels/zwave_js/dialog-zwave_js-update-firmware-node.ts
@@ -110,10 +110,10 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFileUpload}
- label=${this._firmwareFile?.name ??
- this.hass.localize(
+ .label=${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.upload_firmware"
)}
+ .value=${this._firmwareFile}
@file-picked=${this._uploadFile}
>
${this._nodeStatus.is_controller_node
diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts
index 8aee5cb25f..9273ea02f8 100644
--- a/src/panels/config/person/dialog-person-detail.ts
+++ b/src/panels/config/person/dialog-person-detail.ts
@@ -126,6 +126,7 @@ class DialogPersonDetail extends LitElement {
)}
required
>
+