Compare commits

...

22 Commits

Author SHA1 Message Date
Paulus Schoutsen
901677bbdf Bumped version to 20220214.0 2022-02-14 15:33:08 -08:00
Paulus Schoutsen
8bb2374b1b Allow uploading multiple files (#11687) 2022-02-14 17:25:23 -06:00
Paulus Schoutsen
520896a3c2 Try to keep the browsing stack when changing players in media panel (#11681) 2022-02-14 15:21:17 -08:00
Bram Kragten
92db272759 Dont exclude domain for area and device (#11689) 2022-02-14 16:56:50 -06:00
Bram Kragten
fc654d86c6 hassio fixes (#11688) 2022-02-14 22:33:12 +01:00
Bram Kragten
523afe2f6f Another round of paper-dropdown -> mwc-select conversion (#11674)
* Another round of paper-dropdown -> mwc-select conversion

* ha-pick-language-row -> Lit

* Update hui-view-editor.ts

* Cleanup imports

* hassio

* Add explicit imports
2022-02-14 20:08:18 +01:00
Zack Barett
460b9003fc Script Editor to Ha Form (#11601)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-14 11:27:29 -06:00
kpine
2ac0ad1d98 Omit Device info and actions for connected controller nodes (#11673) 2022-02-14 17:06:03 +01:00
Paulus Schoutsen
a321432175 Add TTS to media browser (#11679) 2022-02-14 07:50:44 -08:00
Zack Barett
63c9b3f830 Don't show toggle always on more info (#11640) 2022-02-14 16:21:46 +01:00
Paulus Schoutsen
806b1296b0 Limit types of media that can be uploaded to local media (#11683) 2022-02-14 15:33:21 +01:00
Steve Repsher
7f90ffa82f Set initial focus for some more dialogs (#11676) 2022-02-13 22:02:48 +01:00
Paulus Schoutsen
db33c38e21 Revert compute state display show empty string as unknown (#11677) 2022-02-13 20:26:12 +01:00
Allen Porter
a8c1fdd21e Improve robustness of hls media player (#11672) 2022-02-12 20:21:26 -08:00
lintaba
d86a18b80b hotfix history view on missing state (#11663)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-02-12 14:00:50 -08:00
Michael
bef6591548 Add WORKSPACE_DIRECTORY environment variable to devcontainer and script.core (#11477)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-02-12 07:30:19 +01:00
Bram Kragten
e1c07f109c Filter fixes (#11664) 2022-02-11 23:24:29 +01:00
Zack Barett
fb66d224ae Numerical State to HA-Form (#11646)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-11 22:39:33 +01:00
Paulus Schoutsen
ee1fd3e865 Allow adding Zigbee/Zwave device (#11650) 2022-02-11 19:49:16 +01:00
Bram Kragten
a9bfea233c Improve search and filters on mobile + fix close button in search field (#11662)
Co-authored-by: Zack <zackbarett@hey.com>
2022-02-11 18:34:50 +00:00
Shay Levy
35cc291118 Add support for media player assumed state (#11642) 2022-02-11 08:42:22 -08:00
Zack Barett
db7cac5782 Fix Lovelace Empty Menu when not advanced or admin (#11660) 2022-02-11 10:31:45 -06:00
95 changed files with 2419 additions and 1692 deletions

View File

@@ -16,6 +16,9 @@
"runem.lit-plugin", "runem.lit-plugin",
"ms-python.vscode-pylance" "ms-python.vscode-pylance"
], ],
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"settings": { "settings": {
"terminal.integrated.shell.linux": "/bin/bash", "terminal.integrated.shell.linux": "/bin/bash",
"files.eol": "\n", "files.eol": "\n",

View File

@@ -33,6 +33,10 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
require.resolve( require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts") path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
), ),
isHassioBuild &&
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
),
].filter(Boolean); ].filter(Boolean);
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({

View File

@@ -61,6 +61,12 @@ const SCHEMAS: {
select: { options: ["Everyone Home", "Some Home", "All gone"] }, select: { options: ["Everyone Home", "Some Home", "All gone"] },
}, },
}, },
{
name: "icon",
selector: {
icon: {},
},
},
], ],
}, },
{ {

View File

@@ -72,6 +72,7 @@ const SCHEMAS: {
name: "Select", name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } }, selector: { select: { options: ["Option 1", "Option 2"] } },
}, },
icon: { name: "Icon", selector: { icon: {} } },
}, },
}, },
]; ];

View File

@@ -221,13 +221,14 @@ class HassioAddonStore extends LitElement {
margin-top: 24px; margin-top: 24px;
} }
.search { .search {
padding: 0 16px; position: sticky;
background: var(--sidebar-background-color); top: 0;
border-bottom: 1px solid var(--divider-color); z-index: 2;
} }
.search search-input { search-input {
position: relative; display: block;
top: 2px; --mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
} }
.advanced { .advanced {
padding: 12px; padding: 12px;

View File

@@ -1,7 +1,6 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@material/mwc-select";
import "@polymer/paper-item/paper-item"; import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-listbox/paper-listbox";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -11,7 +10,7 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "web-animations-js/web-animations-next-lite.min"; import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
@@ -57,49 +56,44 @@ class HassioAddonAudio extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
${this._inputDevices &&
<paper-dropdown-menu html`<mwc-select
.label=${this.supervisor.localize( .label=${this.supervisor.localize(
"addon.configuration.audio.input" "addon.configuration.audio.input"
)} )}
@iron-select=${this._setInputDevice} @selected=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedInput!}
> >
<paper-listbox ${this._inputDevices.map(
slot="dropdown-content" (item) => html`
attr-for-selected="device" <mwc-list-item .value=${item.device || ""}>
.selected=${this._selectedInput!} ${item.name}
> </mwc-list-item>
${this._inputDevices && `
this._inputDevices.map( )}
(item) => html` </mwc-select>`}
<paper-item device=${item.device || ""}> ${this._outputDevices &&
${item.name} html`<mwc-select
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
.label=${this.supervisor.localize( .label=${this.supervisor.localize(
"addon.configuration.audio.output" "addon.configuration.audio.output"
)} )}
@iron-select=${this._setOutputDevice} @selected=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedOutput!}
> >
<paper-listbox ${this._outputDevices.map(
slot="dropdown-content" (item) => html`
attr-for-selected="device" <mwc-list-item .value=${item.device || ""}
.selected=${this._selectedOutput!} >${item.name}</mwc-list-item
> >
${this._outputDevices && `
this._outputDevices.map( )}
(item) => html` </mwc-select>`}
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button @click=${this._saveSettings}> <ha-progress-button @click=${this._saveSettings}>
@@ -126,24 +120,30 @@ class HassioAddonAudio extends LitElement {
.card-actions { .card-actions {
text-align: right; text-align: right;
} }
mwc-select {
width: 100%;
}
mwc-select:last-child {
margin-top: 8px;
}
`, `,
]; ];
} }
protected update(changedProperties: PropertyValues): void { protected willUpdate(changedProperties: PropertyValues): void {
super.update(changedProperties); super.willUpdate(changedProperties);
if (changedProperties.has("addon")) { if (changedProperties.has("addon")) {
this._addonChanged(); this._addonChanged();
} }
} }
private _setInputDevice(ev): void { private _setInputDevice(ev): void {
const device = ev.detail.item.getAttribute("device"); const device = ev.target.value;
this._selectedInput = device; this._selectedInput = device;
} }
private _setOutputDevice(ev): void { private _setOutputDevice(ev): void {
const device = ev.detail.item.getAttribute("device"); const device = ev.target.value;
this._selectedOutput = device; this._selectedOutput = device;
} }

View File

@@ -148,7 +148,6 @@ export class HassioUpdate extends LitElement {
} }
ha-settings-row { ha-settings-row {
padding: 0; padding: 0;
--paper-item-body-two-line-min-height: 32px;
} }
`, `,
]; ];

View File

@@ -1,6 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-item/paper-item"; import "@material/mwc-select";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -90,18 +89,19 @@ class HassioDatadiskDialog extends LitElement {
)} )}
<br /><br /> <br /><br />
<paper-dropdown-menu <mwc-select
.label=${this.dialogParams.supervisor.localize( .label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device" "dialog.datadisk_move.select_device"
)} )}
@value-changed=${this._select_device} @selected=${this._select_device}
> >
<paper-listbox slot="dropdown-content"> ${this.devices.map(
${this.devices.map( (device) =>
(device) => html`<paper-item>${device}</paper-item>` html`<mwc-list-item .value=${device}
)} >${device}</mwc-list-item
</paper-listbox> >`
</paper-dropdown-menu> )}
</mwc-select>
` `
: this.devices === undefined : this.devices === undefined
? this.dialogParams.supervisor.localize( ? this.dialogParams.supervisor.localize(
@@ -130,8 +130,8 @@ class HassioDatadiskDialog extends LitElement {
`; `;
} }
private _select_device(event) { private _select_device(ev) {
this.selectedDevice = event.detail.value; this.selectedDevice = ev.target.value;
} }
private async _moveDatadisk() { private async _moveDatadisk() {

View File

@@ -178,7 +178,7 @@ class HassioHardwareDialog extends LitElement {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
} }
search-input { search-input {
margin: 0 16px; margin: 8px 16px 0;
display: block; display: block;
} }
.device-property { .device-property {

View File

@@ -1,7 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
@@ -73,24 +70,19 @@ class HassioSupervisorLog extends LitElement {
: ""} : ""}
${this.hass.userData?.showAdvanced ${this.hass.userData?.showAdvanced
? html` ? html`
<paper-dropdown-menu <mwc-select
.label=${this.supervisor.localize("system.log.log_provider")} .label=${this.supervisor.localize("system.log.log_provider")}
@iron-select=${this._setLogProvider} @selected=${this._setLogProvider}
.value=${this._selectedLogProvider}
> >
<paper-listbox ${logProviders.map(
slot="dropdown-content" (provider) => html`
attr-for-selected="provider" <mwc-list-item .value=${provider.key}>
.selected=${this._selectedLogProvider} ${provider.name}
> </mwc-list-item>
${logProviders.map( `
(provider) => html` )}
<paper-item provider=${provider.key}> </mwc-select>
${provider.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
` `
: ""} : ""}
@@ -110,7 +102,7 @@ class HassioSupervisorLog extends LitElement {
} }
private async _setLogProvider(ev): Promise<void> { private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider"); const provider = ev.target.value;
this._selectedLogProvider = provider; this._selectedLogProvider = provider;
this._loadData(); this._loadData();
} }
@@ -153,9 +145,9 @@ class HassioSupervisorLog extends LitElement {
pre { pre {
white-space: pre-wrap; white-space: pre-wrap;
} }
paper-dropdown-menu { mwc-select {
padding: 0 2%; width: 100%;
width: 96%; margin-bottom: 4px;
} }
`, `,
]; ];

View File

@@ -10,7 +10,6 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert"; import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";

View File

@@ -4,6 +4,8 @@
# Stop on errors # Stop on errors
set -e set -e
WD="${WORKSPACE_DIRECTORY:=/workspaces/frontend}"
if [ -z "${DEVCONTAINER}" ]; then if [ -z "${DEVCONTAINER}" ]; then
echo "This task should only run inside a devcontainer, for local install HA Core in a venv." echo "This task should only run inside a devcontainer, for local install HA Core in a venv."
exit 1 exit 1
@@ -16,9 +18,9 @@ if [ -z $(which hass) ]; then
git+git://github.com/home-assistant/home-assistant.git@dev git+git://github.com/home-assistant/home-assistant.git@dev
fi fi
if [ ! -d "/workspaces/frontend/config" ]; then if [ ! -d "${WD}/config" ]; then
echo "Creating default configuration." echo "Creating default configuration."
mkdir -p "/workspaces/frontend/config"; mkdir -p "${WD}/config";
hass --script ensure_config -c config hass --script ensure_config -c config
echo "demo: echo "demo:
@@ -26,24 +28,24 @@ logger:
default: info default: info
logs: logs:
homeassistant.components.frontend: debug homeassistant.components.frontend: debug
" >> /workspaces/frontend/config/configuration.yaml " >> "${WD}/config/configuration.yaml"
if [ ! -z "${HASSIO}" ]; then if [ ! -z "${HASSIO}" ]; then
echo " echo "
# frontend: # frontend:
# development_repo: /workspaces/frontend # development_repo: ${WD}
hassio: hassio:
development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
else else
echo " echo "
frontend: frontend:
development_repo: /workspaces/frontend development_repo: ${WD}
# hassio: # hassio:
# development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml # development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
fi fi
fi fi
hass -c /workspaces/frontend/config hass -c "${WD}/config"

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = home-assistant-frontend name = home-assistant-frontend
version = 20220203.0 version = 20220214.0
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0

View File

@@ -1,4 +1,4 @@
import { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
export const canToggleDomain = (hass: HomeAssistant, domain: string) => { export const canToggleDomain = (hass: HomeAssistant, domain: string) => {
const services = hass.services[domain]; const services = hass.services[domain];

View File

@@ -1,14 +1,30 @@
import { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { canToggleDomain } from "./can_toggle_domain"; import { canToggleDomain } from "./can_toggle_domain";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature"; import { supportsFeature } from "./supports-feature";
export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => { export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "group") { if (domain === "group") {
return stateObj.state === "on" || stateObj.state === "off"; if (
stateObj.attributes?.entity_id?.some((entity) => {
const entityStateObj = hass.states[entity];
if (!entityStateObj) {
return false;
}
const entityDomain = computeStateDomain(entityStateObj);
return canToggleDomain(hass, entityDomain);
})
) {
return stateObj.state === "on" || stateObj.state === "off";
}
return false;
} }
if (domain === "climate") { if (domain === "climate") {
return supportsFeature(stateObj, 4096); return supportsFeature(stateObj, 4096);
} }

View File

@@ -123,7 +123,11 @@ export const computeStateDisplay = (
domain === "scene" || domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp") (domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) { ) {
return formatDateTime(new Date(compareState), locale); try {
return formatDateTime(new Date(compareState), locale);
} catch (_err) {
return compareState;
}
} }
return ( return (

View File

@@ -14,6 +14,9 @@ class SearchInput extends LitElement {
@property() public filter?: string; @property() public filter?: string;
@property({ type: Boolean })
public suffix = false;
@property({ type: Boolean }) @property({ type: Boolean })
public autofocus = false; public autofocus = false;
@@ -33,7 +36,7 @@ class SearchInput extends LitElement {
.label=${this.label || "Search"} .label=${this.label || "Search"}
.value=${this.filter || ""} .value=${this.filter || ""}
.icon=${true} .icon=${true}
.iconTrailing=${this.filter} .iconTrailing=${this.filter || this.suffix}
@input=${this._filterInputChanged} @input=${this._filterInputChanged}
> >
<slot name="prefix" slot="leadingIcon"> <slot name="prefix" slot="leadingIcon">
@@ -43,16 +46,18 @@ class SearchInput extends LitElement {
.path=${mdiMagnify} .path=${mdiMagnify}
></ha-svg-icon> ></ha-svg-icon>
</slot> </slot>
${this.filter && <div class="trailing" slot="trailingIcon">
html` ${this.filter &&
<ha-icon-button html`
slot="trailingIcon" <ha-icon-button
@click=${this._clearSearch} @click=${this._clearSearch}
.label=${this.hass.localize("ui.common.clear")} .label=${this.hass.localize("ui.common.clear")}
.path=${mdiClose} .path=${mdiClose}
class="clear-button" class="clear-button"
></ha-icon-button> ></ha-icon-button>
`} `}
<slot name="suffix"></slot>
</div>
</ha-textfield> </ha-textfield>
`; `;
} }
@@ -81,15 +86,16 @@ class SearchInput extends LitElement {
ha-svg-icon { ha-svg-icon {
outline: none; outline: none;
} }
ha-icon-button {
--mdc-icon-button-size: 24px;
}
.clear-button { .clear-button {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
} }
ha-textfield { ha-textfield {
display: inherit; display: inherit;
} }
.trailing {
display: flex;
align-items: center;
}
`; `;
} }
} }

View File

@@ -1,6 +1,4 @@
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; import { ComboBoxLitRenderer } from "lit-vaadin-helpers";

View File

@@ -4,6 +4,7 @@ import { mdiFilterVariant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } 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 { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { computeDeviceName } from "../data/device_registry"; import { computeDeviceName } from "../data/device_registry";
import { findRelated, RelatedResult } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
@@ -65,6 +66,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.fullwidth=${this.narrow} .fullwidth=${this.narrow}
.corner=${this.corner} .corner=${this.corner}
@closed=${this._onClosed} @closed=${this._onClosed}
@input=${stopPropagation}
> >
<ha-area-picker <ha-area-picker
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -74,6 +76,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.area} .value=${this.value?.area}
no-add no-add
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
@click=${this._preventDefault}
></ha-area-picker> ></ha-area-picker>
<ha-device-picker <ha-device-picker
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -82,6 +85,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this.value?.device} .value=${this.value?.device}
@value-changed=${this._devicePicked} @value-changed=${this._devicePicked}
@click=${this._preventDefault}
></ha-device-picker> ></ha-device-picker>
<ha-entity-picker <ha-entity-picker
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -91,6 +95,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
.value=${this.value?.entity} .value=${this.value?.entity}
.excludeDomains=${this.excludeDomains} .excludeDomains=${this.excludeDomains}
@value-changed=${this._entityPicked} @value-changed=${this._entityPicked}
@click=${this._preventDefault}
></ha-entity-picker> ></ha-entity-picker>
</mwc-menu-surface> </mwc-menu-surface>
`; `;
@@ -103,11 +108,17 @@ export class HaRelatedFilterButtonMenu extends LitElement {
this._open = true; this._open = true;
} }
private _onClosed(): void { private _onClosed(ev): void {
ev.stopPropagation();
this._open = false; this._open = false;
} }
private _preventDefault(ev) {
ev.preventDefault();
}
private async _entityPicked(ev: CustomEvent) { private async _entityPicked(ev: CustomEvent) {
ev.stopPropagation();
const entityId = ev.detail.value; const entityId = ev.detail.value;
if (!entityId) { if (!entityId) {
fireEvent(this, "related-changed", { value: undefined }); fireEvent(this, "related-changed", { value: undefined });
@@ -127,6 +138,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
} }
private async _devicePicked(ev: CustomEvent) { private async _devicePicked(ev: CustomEvent) {
ev.stopPropagation();
const deviceId = ev.detail.value; const deviceId = ev.detail.value;
if (!deviceId) { if (!deviceId) {
fireEvent(this, "related-changed", { value: undefined }); fireEvent(this, "related-changed", { value: undefined });
@@ -150,6 +162,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
} }
private async _areaPicked(ev: CustomEvent) { private async _areaPicked(ev: CustomEvent) {
ev.stopPropagation();
const areaId = ev.detail.value; const areaId = ev.detail.value;
if (!areaId) { if (!areaId) {
fireEvent(this, "related-changed", { value: undefined }); fireEvent(this, "related-changed", { value: undefined });
@@ -173,9 +186,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
:host { :host {
display: inline-block; display: inline-block;
position: relative; position: relative;
} --mdc-menu-min-width: 250px;
:host([narrow]) {
position: static;
} }
ha-area-picker, ha-area-picker,
ha-device-picker, ha-device-picker,
@@ -185,8 +196,15 @@ export class HaRelatedFilterButtonMenu extends LitElement {
padding: 4px 16px; padding: 4px 16px;
box-sizing: border-box; box-sizing: border-box;
} }
ha-area-picker {
padding-top: 16px;
}
ha-entity-picker {
padding-bottom: 16px;
}
:host([narrow]) ha-area-picker, :host([narrow]) ha-area-picker,
:host([narrow]) ha-device-picker { :host([narrow]) ha-device-picker,
:host([narrow]) ha-entity-picker {
width: 100%; width: 100%;
} }
`; `;

View File

@@ -32,7 +32,12 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public computeError?: (schema: HaFormSchema, error) => string; @property() public computeError?: (schema: HaFormSchema, error) => string;
@property() public computeLabel?: (schema: HaFormSchema) => string; @property() public computeLabel?: (
schema: HaFormSchema,
data?: HaFormDataContainer
) => string;
@property() public computeHelper?: (schema: HaFormSchema) => string;
public focus() { public focus() {
const root = this.shadowRoot?.querySelector(".root"); const root = this.shadowRoot?.querySelector(".root");
@@ -71,6 +76,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""} : ""}
${this.schema.map((item) => { ${this.schema.map((item) => {
const error = getValue(this.error, item); const error = getValue(this.error, item);
return html` return html`
${error ${error
? html` ? html`
@@ -85,14 +91,15 @@ export class HaForm extends LitElement implements HaFormElement {
.hass=${this.hass} .hass=${this.hass}
.selector=${item.selector} .selector=${item.selector}
.value=${getValue(this.data, item)} .value=${getValue(this.data, item)}
.label=${this._computeLabel(item)} .label=${this._computeLabel(item, this.data)}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this._computeHelper(item)}
.required=${item.required || false} .required=${item.required || false}
></ha-selector>` ></ha-selector>`
: dynamicElement(`ha-form-${item.type}`, { : dynamicElement(`ha-form-${item.type}`, {
schema: item, schema: item,
data: getValue(this.data, item), data: getValue(this.data, item),
label: this._computeLabel(item), label: this._computeLabel(item, this.data),
disabled: this.disabled, disabled: this.disabled,
})} })}
`; `;
@@ -107,6 +114,7 @@ export class HaForm extends LitElement implements HaFormElement {
root.addEventListener("value-changed", (ev) => { root.addEventListener("value-changed", (ev) => {
ev.stopPropagation(); ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema; const schema = (ev.target as HaFormElement).schema as HaFormSchema;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.data, [schema.name]: ev.detail.value }, value: { ...this.data, [schema.name]: ev.detail.value },
}); });
@@ -114,14 +122,18 @@ export class HaForm extends LitElement implements HaFormElement {
return root; return root;
} }
private _computeLabel(schema: HaFormSchema) { private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) {
return this.computeLabel return this.computeLabel
? this.computeLabel(schema) ? this.computeLabel(schema, data)
: schema : schema
? schema.name ? schema.name
: ""; : "";
} }
private _computeHelper(schema: HaFormSchema) {
return this.computeHelper ? this.computeHelper(schema) : "";
}
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error; return this.computeError ? this.computeError(error, schema) : error;
} }

View File

@@ -43,6 +43,8 @@ class HaHLSPlayer extends LitElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _errorIsFatal = false;
private _hlsPolyfillInstance?: HlsLite; private _hlsPolyfillInstance?: HlsLite;
private _exoPlayer = false; private _exoPlayer = false;
@@ -53,6 +55,7 @@ class HaHLSPlayer extends LitElement {
super.connectedCallback(); super.connectedCallback();
HaHLSPlayer.streamCount += 1; HaHLSPlayer.streamCount += 1;
if (this.hasUpdated) { if (this.hasUpdated) {
this._resetError();
this._startHls(); this._startHls();
} }
} }
@@ -64,16 +67,23 @@ class HaHLSPlayer extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html` return html`
<video ${this._error
?autoplay=${this.autoPlay} ? html`<ha-alert
.muted=${this.muted} alert-type="error"
?playsinline=${this.playsInline} class=${this._errorIsFatal ? "fatal" : "retry"}
?controls=${this.controls} >
></video> ${this._error}
</ha-alert>`
: ""}
${!this._errorIsFatal
? html`<video
?autoplay=${this.autoPlay}
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
></video>`
: ""}
`; `;
} }
@@ -87,12 +97,11 @@ class HaHLSPlayer extends LitElement {
} }
this._cleanUp(); this._cleanUp();
this._resetError();
this._startHls(); this._startHls();
} }
private async _startHls(): Promise<void> { private async _startHls(): Promise<void> {
this._error = undefined;
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min")) const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
@@ -110,8 +119,8 @@ class HaHLSPlayer extends LitElement {
} }
if (!hlsSupported) { if (!hlsSupported) {
this._error = this.hass.localize( this._setFatalError(
"ui.components.media-browser.video_not_supported" this.hass.localize("ui.components.media-browser.video_not_supported")
); );
return; return;
} }
@@ -219,9 +228,16 @@ class HaHLSPlayer extends LitElement {
this._hlsPolyfillInstance = hls; this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl); hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => { hls.on(Hls.Events.MEDIA_ATTACHED, () => {
this._resetError();
hls.loadSource(url); hls.loadSource(url);
}); });
hls.on(Hls.Events.ERROR, (_, data: any) => { hls.on(Hls.Events.FRAG_LOADED, (_event, _data: any) => {
this._resetError();
});
hls.on(Hls.Events.ERROR, (_event, data: any) => {
// Some errors are recovered automatically by the hls player itself, and the others handled
// in this function require special actions to recover. Errors retried in this function
// are done with backoff to not cause unecessary failures.
if (!data.fatal) { if (!data.fatal) {
return; return;
} }
@@ -241,22 +257,22 @@ class HaHLSPlayer extends LitElement {
error += " (" + data.response.code + ")"; error += " (" + data.response.code + ")";
} }
} }
this._error = error; this._setRetryableError(error);
return; break;
} }
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT: case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
this._error = "Timeout while starting stream"; this._setRetryableError("Timeout while starting stream");
return; break;
default: default:
this._error = "Unknown stream network error (" + data.details + ")"; this._setRetryableError("Stream network error");
return; break;
} }
this._error = "Error with media stream contents (" + data.details + ")"; hls.startLoad();
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
this._error = "Error with media stream contents (" + data.details + ")"; this._setRetryableError("Error with media stream contents");
hls.recoverMediaError();
} else { } else {
this._error = this._setFatalError("Error playing stream");
"Unknown error with stream (" + data.type + ", " + data.details + ")";
} }
}); });
} }
@@ -284,6 +300,21 @@ class HaHLSPlayer extends LitElement {
} }
} }
private _resetError() {
this._error = undefined;
this._errorIsFatal = false;
}
private _setFatalError(errorMessage: string) {
this._error = errorMessage;
this._errorIsFatal = true;
}
private _setRetryableError(errorMessage: string) {
this._error = errorMessage;
this._errorIsFatal = false;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host, :host,
@@ -296,10 +327,14 @@ class HaHLSPlayer extends LitElement {
max-height: var(--video-max-height, calc(100vh - 97px)); max-height: var(--video-max-height, calc(100vh - 97px));
} }
ha-alert { .fatal {
display: block; display: block;
padding: 100px 16px; padding: 100px 16px;
} }
.retry {
display: block;
}
`; `;
} }
} }

View File

@@ -1,28 +0,0 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import { PolymerElement } from "@polymer/polymer";
import { Constructor } from "../types";
const paperDropdownClass = customElements.get(
"paper-dropdown-menu"
) as Constructor<PolymerElement>;
// patches paper drop down to properly support RTL - https://github.com/PolymerElements/paper-dropdown-menu/issues/183
export class HaPaperDropdownClass extends paperDropdownClass {
public ready() {
super.ready();
// wait to check for direction since otherwise direction is wrong even though top level is RTL
setTimeout(() => {
if (window.getComputedStyle(this).direction === "rtl") {
this.style.textAlign = "right";
}
}, 100);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-paper-dropdown-menu": HaPaperDropdownClass;
}
}
customElements.define("ha-paper-dropdown-menu", HaPaperDropdownClass);

View File

@@ -0,0 +1,39 @@
import "../ha-icon-picker";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../types";
import { IconSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: IconSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
@value-changed=${this._valueChanged}
></ha-icon-picker>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", { value: ev.detail.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-icon": HaIconSelector;
}
}

View File

@@ -2,7 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation"; import { stopPropagation } from "../../common/dom/stop_propagation";
import { SelectSelector } from "../../data/selector"; import { SelectOption, SelectSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "@material/mwc-select/mwc-select"; import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
@@ -17,6 +17,8 @@ export class HaSelectSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
@@ -25,15 +27,17 @@ export class HaSelectSelector extends LitElement {
naturalMenuWidth naturalMenuWidth
.label=${this.label} .label=${this.label}
.value=${this.value} .value=${this.value}
.helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@closed=${stopPropagation} @closed=${stopPropagation}
@selected=${this._valueChanged} @selected=${this._valueChanged}
> >
${this.selector.select.options.map( ${this.selector.select.options.map((item: string | SelectOption) => {
(item: string) => html` const value = typeof item === "object" ? item.value : item;
<mwc-list-item .value=${item}>${item}</mwc-list-item> const label = typeof item === "object" ? item.label : item;
`
)} return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</mwc-select>`; </mwc-select>`;
} }

View File

@@ -17,6 +17,7 @@ import "./ha-selector-select";
import "./ha-selector-target"; import "./ha-selector-target";
import "./ha-selector-text"; import "./ha-selector-text";
import "./ha-selector-time"; import "./ha-selector-time";
import "./ha-selector-icon";
@customElement("ha-selector") @customElement("ha-selector")
export class HaSelector extends LitElement { export class HaSelector extends LitElement {
@@ -28,6 +29,8 @@ export class HaSelector extends LitElement {
@property() public label?: string; @property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: any; @property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@@ -52,6 +55,7 @@ export class HaSelector extends LitElement {
placeholder: this.placeholder, placeholder: this.placeholder,
disabled: this.disabled, disabled: this.disabled,
required: this.required, required: this.required,
helper: this.helper,
id: "selector", id: "selector",
})} })}
`; `;

View File

@@ -68,6 +68,14 @@ export class HaTextField extends TextFieldBase {
:host([no-spinner]) input[type="number"] { :host([no-spinner]) input[type="number"] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }
.mdc-text-field__ripple {
overflow: hidden;
}
.mdc-text-field {
overflow: var(--text-field-overflow);
}
`, `,
]; ];
} }

View File

@@ -0,0 +1,230 @@
import "@material/mwc-select";
import "@material/mwc-list/mwc-list-item";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { fetchCloudStatus, updateCloudPref } from "../../data/cloud";
import {
CloudTTSInfo,
getCloudTTSInfo,
getCloudTtsLanguages,
getCloudTtsSupportedGenders,
} from "../../data/cloud/tts";
import { MediaPlayerBrowseAction } from "../../data/media-player";
import { HomeAssistant } from "../../types";
import "../ha-textarea";
import { buttonLinkStyle } from "../../resources/styles";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { LocalStorage } from "../../common/decorators/local-storage";
@customElement("ha-browse-media-tts")
class BrowseMediaTTS extends LitElement {
@property() public hass!: HomeAssistant;
@property() public item;
@property() public action!: MediaPlayerBrowseAction;
@state() private _cloudDefaultOptions?: [string, string];
@state() private _cloudOptions?: [string, string];
@state() private _cloudTTSInfo?: CloudTTSInfo;
@LocalStorage("cloudTtsTryMessage", false, false) private _message!: string;
protected render() {
return html`
<ha-textarea
autogrow
.label=${this.hass.localize("ui.panel.media-browser.tts.message")}
.value=${this._message ||
this.hass.localize("ui.panel.media-browser.tts.example_message", {
name: this.hass.user?.name || "",
})}
>
</ha-textarea>
${this._cloudDefaultOptions ? this._renderCloudOptions() : ""}
<div class="actions">
${this._cloudDefaultOptions &&
(this._cloudDefaultOptions![0] !== this._cloudOptions![0] ||
this._cloudDefaultOptions![1] !== this._cloudOptions![1])
? html`
<button class="link" @click=${this._storeDefaults}>
${this.hass.localize(
"ui.panel.media-browser.tts.set_as_default"
)}
</button>
`
: html`<span></span>`}
<mwc-button raised label="Say" @click=${this._ttsClicked}></mwc-button>
</div>
`;
}
private _renderCloudOptions() {
const languages = this.getLanguages(this._cloudTTSInfo);
const selectedVoice = this._cloudOptions!;
const genders = this.getSupportedGenders(
selectedVoice[0],
this._cloudTTSInfo,
this.hass.localize
);
return html`
<div class="cloud-options">
<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize("ui.panel.media-browser.tts.language")}
.value=${selectedVoice[0]}
@selected=${this._handleLanguageChange}
>
${languages.map(
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</mwc-select>
<mwc-select
fixedMenuPosition
naturalMenuWidth
.label=${this.hass.localize("ui.panel.media-browser.tts.gender")}
.value=${selectedVoice[1]}
@selected=${this._handleGenderChange}
>
${genders.map(
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</mwc-select>
</div>
`;
}
protected override willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("message")) {
return;
}
// Re-rendering can reset message because textarea content is newer than local storage.
// But we don't want to write every keystroke to local storage.
// So instead we just do it when we're going to render.
const message = this.shadowRoot!.querySelector("ha-textarea")?.value;
if (message !== undefined && message !== this._message) {
this._message = message;
}
}
async _handleLanguageChange(ev) {
if (ev.target.value === this._cloudOptions![0]) {
return;
}
this._cloudOptions = [ev.target.value, this._cloudOptions![1]];
}
async _handleGenderChange(ev) {
if (ev.target.value === this._cloudOptions![1]) {
return;
}
this._cloudOptions = [this._cloudOptions![0], ev.target.value];
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("item")) {
if (this.isCloudItem && !this._cloudTTSInfo) {
getCloudTTSInfo(this.hass).then((info) => {
this._cloudTTSInfo = info;
});
fetchCloudStatus(this.hass).then((status) => {
if (status.logged_in) {
this._cloudDefaultOptions = status.prefs.tts_default_voice;
this._cloudOptions = { ...this._cloudDefaultOptions };
}
});
}
}
}
private getLanguages = memoizeOne(getCloudTtsLanguages);
private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders);
private get isCloudItem(): boolean {
return this.item.media_content_id === "media-source://tts/cloud";
}
private async _ttsClicked(): Promise<void> {
const message = this.shadowRoot!.querySelector("ha-textarea")!.value;
this._message = message;
const item = { ...this.item };
const query = new URLSearchParams();
query.append("message", message);
if (this._cloudOptions) {
query.append("language", this._cloudOptions[0]);
query.append("gender", this._cloudOptions[1]);
}
item.media_content_id += `?${query.toString()}`;
item.can_play = true;
fireEvent(this, "media-picked", { item });
}
private async _storeDefaults() {
const oldDefaults = this._cloudDefaultOptions!;
this._cloudDefaultOptions = [...this._cloudOptions!];
try {
await updateCloudPref(this.hass, {
tts_default_voice: this._cloudDefaultOptions,
});
} catch (err: any) {
this._cloudDefaultOptions = oldDefaults;
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.media-browser.tts.faild_to_store_defaults",
{ error: err.message || err }
),
});
}
}
static override styles = [
buttonLinkStyle,
css`
:host {
margin: 16px auto;
padding: 0 8px;
display: flex;
flex-direction: column;
max-width: 400px;
}
.cloud-options {
margin-top: 16px;
display: flex;
justify-content: space-between;
}
.cloud-options mwc-select {
width: 48%;
}
.actions {
display: flex;
justify-content: space-between;
margin-top: 16px;
}
button.link {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-browse-media-tts": BrowseMediaTTS;
}
}

View File

@@ -1,8 +1,7 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiPlay, mdiPlus } from "@mdi/js"; import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { import {
css, css,
@@ -49,11 +48,20 @@ import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "../ha-fab"; import "../ha-fab";
import { browseLocalMediaPlayer } from "../../data/media_source"; import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import "./ha-browse-media-tts";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"media-picked": MediaPickedEvent; "media-picked": MediaPickedEvent;
"media-browsed": { ids: MediaPlayerItemId[]; current?: MediaPlayerItem }; "media-browsed": {
// Items of the new browse stack
ids: MediaPlayerItemId[];
// Current fetched item for this browse stack
current?: MediaPlayerItem;
// If the new stack should replace the old stack
replace?: boolean;
};
} }
} }
@@ -246,160 +254,160 @@ export class HaMediaPlayerBrowse extends LitElement {
${this._renderError(this._error)} ${this._renderError(this._error)}
</div> </div>
` `
: currentItem.children?.length : isTTSMediaSource(currentItem.media_content_id)
? childrenMediaClass.layout === "grid" ? html`
? html` <ha-browse-media-tts
<div .item=${currentItem}
class="children ${classMap({ .hass=${this.hass}
portrait: .action=${this.action}
childrenMediaClass.thumbnail_ratio === "portrait", ></ha-browse-media-tts>
})}" `
> : !currentItem.children?.length
${currentItem.children.map( ? html`
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${[
"app",
"directory",
].includes(child.media_class)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`
)}
</div>
`
: html`
<mwc-list>
${currentItem.children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
</mwc-list>
`
: html`
<div class="container no-items"> <div class="container no-items">
${this.hass.localize(
"ui.components.media-browser.no_items"
)}
<br />
${currentItem.media_content_id === ${currentItem.media_content_id ===
"media-source://media_source/local/." "media-source://media_source/local/."
? html`<br />${this.hass.localize( ? html`
"ui.components.media-browser.learn_adding_local_media", <div class="highlight-add-button">
"documentation", <span>
html`<a <ha-svg-icon
href=${documentationUrl( .path=${mdiArrowUpRight}
this.hass, ></ha-svg-icon>
"/more-info/local-media/add-media" </span>
<span>
${this.hass.localize(
"ui.components.media-browser.file_management.highlight_button"
)} )}
target="_blank" </span>
rel="noreferrer" </div>
>${this.hass.localize( `
"ui.components.media-browser.documentation" : this.hass.localize(
)}</a "ui.components.media-browser.no_items"
>` )}
)}
<br />
${this.hass.localize(
"ui.components.media-browser.local_media_files"
)}`
: ""}
</div> </div>
` `
: childrenMediaClass.layout === "grid"
? html`
<div
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${currentItem.children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${["app", "directory"].includes(
child.media_class
)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`
)}
</div>
`
: html`
<mwc-list>
${currentItem.children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
</mwc-list>
`
} }
</div> </div>
</div> </div>
@@ -425,8 +433,8 @@ export class HaMediaPlayerBrowse extends LitElement {
if (changedProps.has("entityId")) { if (changedProps.has("entityId")) {
this._setError(undefined); this._setError(undefined);
} } else if (!changedProps.has("navigateIds")) {
if (!changedProps.has("navigateIds")) { // Neither entity ID or navigateIDs changed, nothing to fetch
return; return;
} }
@@ -435,6 +443,7 @@ export class HaMediaPlayerBrowse extends LitElement {
const oldNavigateIds = changedProps.get("navigateIds") as const oldNavigateIds = changedProps.get("navigateIds") as
| this["navigateIds"] | this["navigateIds"]
| undefined; | undefined;
const navigateIds = this.navigateIds;
// We're navigating. Reset the shizzle. // We're navigating. Reset the shizzle.
this._content?.scrollTo(0, 0); this._content?.scrollTo(0, 0);
@@ -443,11 +452,9 @@ export class HaMediaPlayerBrowse extends LitElement {
const oldParentItem = this._parentItem; const oldParentItem = this._parentItem;
this._currentItem = undefined; this._currentItem = undefined;
this._parentItem = undefined; this._parentItem = undefined;
const currentId = this.navigateIds[this.navigateIds.length - 1]; const currentId = navigateIds[navigateIds.length - 1];
const parentId = const parentId =
this.navigateIds.length > 1 navigateIds.length > 1 ? navigateIds[navigateIds.length - 2] : undefined;
? this.navigateIds[this.navigateIds.length - 2]
: undefined;
let currentProm: Promise<MediaPlayerItem> | undefined; let currentProm: Promise<MediaPlayerItem> | undefined;
let parentProm: Promise<MediaPlayerItem> | undefined; let parentProm: Promise<MediaPlayerItem> | undefined;
@@ -456,9 +463,9 @@ export class HaMediaPlayerBrowse extends LitElement {
if ( if (
// Check if we navigated to a child // Check if we navigated to a child
oldNavigateIds && oldNavigateIds &&
this.navigateIds.length > oldNavigateIds.length && navigateIds.length === oldNavigateIds.length + 1 &&
oldNavigateIds.every((oldVal, idx) => { oldNavigateIds.every((oldVal, idx) => {
const curVal = this.navigateIds[idx]; const curVal = navigateIds[idx];
return ( return (
curVal.media_content_id === oldVal.media_content_id && curVal.media_content_id === oldVal.media_content_id &&
curVal.media_content_type === oldVal.media_content_type curVal.media_content_type === oldVal.media_content_type
@@ -469,8 +476,8 @@ export class HaMediaPlayerBrowse extends LitElement {
} else if ( } else if (
// Check if we navigated to a parent // Check if we navigated to a parent
oldNavigateIds && oldNavigateIds &&
this.navigateIds.length < oldNavigateIds.length && navigateIds.length === oldNavigateIds.length - 1 &&
this.navigateIds.every((curVal, idx) => { navigateIds.every((curVal, idx) => {
const oldVal = oldNavigateIds[idx]; const oldVal = oldNavigateIds[idx];
return ( return (
curVal.media_content_id === oldVal.media_content_id && curVal.media_content_id === oldVal.media_content_id &&
@@ -493,11 +500,33 @@ export class HaMediaPlayerBrowse extends LitElement {
(item) => { (item) => {
this._currentItem = item; this._currentItem = item;
fireEvent(this, "media-browsed", { fireEvent(this, "media-browsed", {
ids: this.navigateIds, ids: navigateIds,
current: item, current: item,
}); });
}, },
(err) => this._setError(err) (err) => {
// When we change entity ID, we will first try to see if the new entity is
// able to resolve the new path. If that results in an error, browse the root.
const isNewEntityWithSamePath =
oldNavigateIds &&
changedProps.has("entityId") &&
navigateIds.length === oldNavigateIds.length &&
oldNavigateIds.every(
(oldItem, idx) =>
navigateIds[idx].media_content_id === oldItem.media_content_id &&
navigateIds[idx].media_content_type === oldItem.media_content_type
);
if (isNewEntityWithSamePath) {
fireEvent(this, "media-browsed", {
ids: [
{ media_content_id: undefined, media_content_type: undefined },
],
replace: true,
});
} else {
this._setError(err);
}
}
); );
// Fetch parent // Fetch parent
if (!parentProm && parentId !== undefined) { if (!parentProm && parentId !== undefined) {
@@ -736,6 +765,18 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-left: 32px; padding-left: 32px;
} }
.highlight-add-button {
display: flex;
flex-direction: row-reverse;
margin-right: 48px;
}
.highlight-add-button ha-svg-icon {
position: relative;
top: -0.5em;
margin-left: 8px;
}
.content { .content {
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;

View File

@@ -186,10 +186,3 @@ export const updateCloudAlexaEntityConfig = (
entity_id: entityId, entity_id: entityId,
...values, ...values,
}); });
export interface CloudTTSInfo {
languages: Array<[string, string]>;
}
export const getCloudTTSInfo = (hass: HomeAssistant) =>
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });

70
src/data/cloud/tts.ts Normal file
View File

@@ -0,0 +1,70 @@
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import { translationMetadata } from "../../resources/translations-metadata";
import { HomeAssistant } from "../../types";
export interface CloudTTSInfo {
languages: Array<[string, string]>;
}
export const getCloudTTSInfo = (hass: HomeAssistant) =>
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });
export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
const languages: Array<[string, string]> = [];
if (!info) {
return languages;
}
const seen = new Set<string>();
for (const [lang] of info.languages) {
if (seen.has(lang)) {
continue;
}
seen.add(lang);
let label = lang;
if (lang in translationMetadata.translations) {
label = translationMetadata.translations[lang].nativeName;
} else {
const [langFamily, dialect] = lang.split("-");
if (langFamily in translationMetadata.translations) {
label = `${translationMetadata.translations[langFamily].nativeName}`;
if (langFamily.toLowerCase() !== dialect.toLowerCase()) {
label += ` (${dialect})`;
}
}
}
languages.push([lang, label]);
}
return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
};
export const getCloudTtsSupportedGenders = (
language: string,
info: CloudTTSInfo | undefined,
localize: LocalizeFunc
) => {
const genders: Array<[string, string]> = [];
if (!info) {
return genders;
}
for (const [curLang, gender] of info.languages) {
if (curLang === language) {
genders.push([
gender,
localize(`ui.panel.media-browser.tts.gender_${gender}`) ||
localize(`ui.panel.config.cloud.account.tts.${gender}`) ||
gender,
]);
}
}
return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
};

View File

@@ -261,8 +261,10 @@ export const computeMediaControls = (
}); });
} }
const assumedState = stateObj.attributes.assumed_state === true;
if ( if (
(state === "playing" || state === "paused") && (state === "playing" || state === "paused" || assumedState) &&
supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK) supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
) { ) {
buttons.push({ buttons.push({
@@ -272,14 +274,15 @@ export const computeMediaControls = (
} }
if ( if (
(state === "playing" && !assumedState &&
((state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) || (supportsFeature(stateObj, SUPPORT_PAUSE) ||
supportsFeature(stateObj, SUPPORT_STOP))) || supportsFeature(stateObj, SUPPORT_STOP))) ||
((state === "paused" || state === "idle") && ((state === "paused" || state === "idle") &&
supportsFeature(stateObj, SUPPORT_PLAY)) || supportsFeature(stateObj, SUPPORT_PLAY)) ||
(state === "on" && (state === "on" &&
(supportsFeature(stateObj, SUPPORT_PLAY) || (supportsFeature(stateObj, SUPPORT_PLAY) ||
supportsFeature(stateObj, SUPPORT_PAUSE))) supportsFeature(stateObj, SUPPORT_PAUSE))))
) { ) {
buttons.push({ buttons.push({
icon: icon:
@@ -299,8 +302,29 @@ export const computeMediaControls = (
}); });
} }
if (assumedState && supportsFeature(stateObj, SUPPORT_PLAY)) {
buttons.push({
icon: mdiPlay,
action: "media_play",
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_PAUSE)) {
buttons.push({
icon: mdiPause,
action: "media_pause",
});
}
if (assumedState && supportsFeature(stateObj, SUPPORT_STOP)) {
buttons.push({
icon: mdiStop,
action: "media_stop",
});
}
if ( if (
(state === "playing" || state === "paused") && (state === "playing" || state === "paused" || assumedState) &&
supportsFeature(stateObj, SUPPORT_NEXT_TRACK) supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
) { ) {
buttons.push({ buttons.push({

View File

@@ -12,7 +12,8 @@ export type Selector =
| ActionSelector | ActionSelector
| StringSelector | StringSelector
| ObjectSelector | ObjectSelector
| SelectSelector; | SelectSelector
| IconSelector;
export interface EntitySelector { export interface EntitySelector {
entity: { entity: {
@@ -133,8 +134,18 @@ export interface ObjectSelector {
object: {}; object: {};
} }
export interface SelectOption {
value: string;
label: string;
}
export interface SelectSelector { export interface SelectSelector {
select: { select: {
options: string[]; options: string[] | SelectOption[];
}; };
} }
export interface IconSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
icon: {};
}

View File

@@ -10,3 +10,11 @@ export const convertTextToSpeech = (
options?: Record<string, unknown>; options?: Record<string, unknown>;
} }
) => hass.callApi<{ url: string; path: string }>("POST", "tts_get_url", data); ) => hass.callApi<{ url: string; path: string }>("POST", "tts_get_url", data);
const TTS_MEDIA_SOURCE_PREFIX = "media-source://tts/";
export const isTTSMediaSource = (mediaContentId: string) =>
mediaContentId.startsWith(TTS_MEDIA_SOURCE_PREFIX);
export const getProviderFromTTSMediaSource = (mediaContentId: string) =>
mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length);

View File

@@ -126,6 +126,7 @@ export interface ZWaveJSNodeStatus {
is_routing: boolean | null; is_routing: boolean | null;
zwave_plus_version: number | null; zwave_plus_version: number | null;
highest_security_class: SecurityClass | null; highest_security_class: SecurityClass | null;
is_controller_node: boolean;
} }
export interface ZwaveJSNodeMetadata { export interface ZwaveJSNodeMetadata {

View File

@@ -83,6 +83,7 @@ class DialogConfigEntrySystemOptions extends LitElement {
.checked=${!this._disableNewEntities} .checked=${!this._disableNewEntities}
@change=${this._disableNewEntitiesChanged} @change=${this._disableNewEntitiesChanged}
.disabled=${this._submitting} .disabled=${this._submitting}
dialogInitialFocus
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
${this._allowUpdatePolling() ${this._allowUpdatePolling()

View File

@@ -1,23 +1,27 @@
import "@polymer/paper-item/paper-icon-item"; import "@material/mwc-list/mwc-list";
import "@polymer/paper-item/paper-item-body"; import "@material/mwc-list/mwc-list-item";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
TemplateResult,
PropertyValues, PropertyValues,
TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import "../../common/search/search-input"; import "../../common/search/search-input";
import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize"; import { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-icon-next"; import "../../components/ha-icon-next";
import { getConfigEntries } from "../../data/config_entries";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url"; import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
@@ -26,6 +30,7 @@ import { configFlowContentStyles } from "./styles";
interface HandlerObj { interface HandlerObj {
name: string; name: string;
slug: string; slug: string;
is_add?: boolean;
} }
declare global { declare global {
@@ -77,6 +82,17 @@ class StepFlowPickHandler extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
const handlers = this._getHandlers(); const handlers = this._getHandlers();
const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"]
.filter((domain) => isComponentLoaded(this.hass, domain))
.map((domain) => ({
name: this.hass.localize(
`ui.panel.config.integrations.add_${domain}_device`
),
slug: domain,
is_add: true,
}))
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
return html` return html`
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2> <h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<search-input <search-input
@@ -87,37 +103,20 @@ class StepFlowPickHandler extends LitElement {
.label=${this.hass.localize("ui.panel.config.integrations.search")} .label=${this.hass.localize("ui.panel.config.integrations.search")}
@keypress=${this._maybeSubmit} @keypress=${this._maybeSubmit}
></search-input> ></search-input>
<div <mwc-list
style=${styleMap({ style=${styleMap({
width: `${this._width}px`, width: `${this._width}px`,
height: `${this._height}px`, height: `${this._height}px`,
})} })}
> >
${addDeviceRows.length
? html`
${addDeviceRows.map((handler) => this._renderRow(handler))}
<li divider padded class="divider" role="separator"></li>
`
: ""}
${handlers.length ${handlers.length
? handlers.map( ? handlers.map((handler) => this._renderRow(handler))
(handler: HandlerObj) =>
html`
<paper-icon-item
@click=${this._handlerPicked}
.handler=${handler}
>
<img
slot="item-icon"
loading="lazy"
src=${brandsUrl({
domain: handler.slug,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<paper-item-body> ${handler.name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>
`
)
: html` : html`
<p> <p>
${this.hass.localize( ${this.hass.localize(
@@ -140,15 +139,56 @@ class StepFlowPickHandler extends LitElement {
>. >.
</p> </p>
`} `}
</div> </mwc-list>
`;
}
private _renderRow(handler: HandlerObj) {
return html`
<mwc-list-item
graphic="medium"
.hasMeta=${!handler.is_add}
.handler=${handler}
@click=${this._handlerPicked}
>
<img
slot="graphic"
loading="lazy"
src=${brandsUrl({
domain: handler.slug,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<span>${handler.name}</span>
${handler.is_add ? "" : html`<ha-icon-next slot="meta"></ha-icon-next>`}
</mwc-list-item>
`; `;
} }
public willUpdate(changedProps: PropertyValues): void { public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (this._filter === undefined && this.initialFilter !== undefined) { if (this._filter === undefined && this.initialFilter !== undefined) {
this._filter = this.initialFilter; this._filter = this.initialFilter;
} }
super.willUpdate(changedProps); if (this.initialFilter !== undefined && this._filter === "") {
this.initialFilter = undefined;
this._filter = "";
this._width = undefined;
this._height = undefined;
} else if (
this.hasUpdated &&
changedProps.has("_filter") &&
(!this._width || !this._height)
) {
// Store the width and height so that when we search, box doesn't jump
const boundingRect =
this.shadowRoot!.querySelector("mwc-list")!.getBoundingClientRect();
this._width = boundingRect.width;
this._height = boundingRect.height;
}
} }
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
@@ -159,24 +199,6 @@ class StepFlowPickHandler extends LitElement {
); );
} }
protected updated(changedProps) {
super.updated(changedProps);
// Store the width and height so that when we search, box doesn't jump
const div = this.shadowRoot!.querySelector("div")!;
if (!this._width) {
const width = div.clientWidth;
if (width) {
this._width = width;
}
}
if (!this._height) {
const height = div.clientHeight;
if (height) {
this._height = height;
}
}
}
private _getHandlers() { private _getHandlers() {
return this._filterHandlers( return this._filterHandlers(
this.handlers, this.handlers,
@@ -190,8 +212,31 @@ class StepFlowPickHandler extends LitElement {
} }
private async _handlerPicked(ev) { private async _handlerPicked(ev) {
const handler: HandlerObj = ev.currentTarget.handler;
if (handler.is_add) {
if (handler.slug === "zwave_js") {
const entries = await getConfigEntries(this.hass);
const entry = entries.find((ent) => ent.domain === "zwave_js");
if (!entry) {
return;
}
showZWaveJSAddNodeDialog(this, {
entry_id: entry.entry_id,
});
} else if (handler.slug === "zha") {
navigate("/config/zha/add");
}
// This closes dialog.
fireEvent(this, "flow-update");
return;
}
fireEvent(this, "handler-picked", { fireEvent(this, "handler-picked", {
handler: ev.currentTarget.handler.slug, handler: handler.slug,
}); });
} }
@@ -219,27 +264,26 @@ class StepFlowPickHandler extends LitElement {
} }
search-input { search-input {
display: block; display: block;
margin: -12px 16px 0; margin: 16px 16px 0;
} }
ha-icon-next { ha-icon-next {
margin-right: 8px; margin-right: 8px;
} }
div { mwc-list {
overflow: auto; overflow: auto;
max-height: 600px; max-height: 600px;
} }
.divider {
border-bottom-color: var(--divider-color);
}
h2 { h2 {
padding-right: 66px; padding-right: 66px;
} }
@media all and (max-height: 900px) { @media all and (max-height: 900px) {
div { mwc-list {
max-height: calc(100vh - 134px); max-height: calc(100vh - 134px);
} }
} }
paper-icon-item {
cursor: pointer;
margin-bottom: 4px;
}
p { p {
text-align: center; text-align: center;
padding: 16px; padding: 16px;

View File

@@ -615,10 +615,6 @@ class MoreInfoLight extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
paper-item {
cursor: pointer;
}
hr { hr {
border-color: var(--divider-color); border-color: var(--divider-color);
border-bottom: none; border-bottom: none;

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attributes"; import "../../../components/ha-attributes";
@@ -66,14 +66,6 @@ class MoreInfoRemote extends LitElement {
activity: newVal, activity: newVal,
}); });
} }
static get styles(): CSSResultGroup {
return css`
paper-item {
cursor: pointer;
}
`;
}
} }
declare global { declare global {

View File

@@ -241,9 +241,6 @@ class MoreInfoVacuum extends LitElement {
.status-subtitle { .status-subtitle {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
paper-item {
cursor: pointer;
}
.flex-horizontal { .flex-horizontal {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -338,7 +338,9 @@ export class MoreInfoDialog extends LitElement {
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;
} }
.content {
outline: none;
}
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar { ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color); --mdc-theme-primary: var(--app-header-background-color);

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiFilterVariant } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
@@ -157,30 +156,31 @@ export class HaTabsSubpageDataTable extends LitElement {
: hiddenLabel; : hiddenLabel;
const headerToolbar = html`<search-input const headerToolbar = html`<search-input
.hass=${this.hass} .hass=${this.hass}
.filter=${this.filter} .filter=${this.filter}
@value-changed=${this._handleSearchChange} .suffix=${!this.narrow}
.label=${this.searchLabel || @value-changed=${this._handleSearchChange}
this.hass.localize("ui.components.data-table.search")} .label=${this.searchLabel ||
> this.hass.localize("ui.components.data-table.search")}
</search-input> >
<div class="filters"> ${!this.narrow
${filterInfo ? html`<div
? html`<div class="active-filters"> class="filters"
${this.narrow slot="suffix"
? html`<div> @click=${this._preventDefault}
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon> >
<paper-tooltip animation-delay="0" position="left"> ${filterInfo
${filterInfo} ? html`<div class="active-filters">
</paper-tooltip> ${filterInfo}
</div>` <mwc-button @click=${this._clearFilter}>
: filterInfo} ${this.hass.localize("ui.components.data-table.clear")}
<mwc-button @click=${this._clearFilter}> </mwc-button>
${this.hass.localize("ui.components.data-table.clear")} </div>`
</mwc-button> : ""}
</div>` <slot name="filter-menu"></slot>
: ""}<slot name="filter-menu"></slot> </div>`
</div>`; : ""}
</search-input>`;
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@@ -195,7 +195,16 @@ export class HaTabsSubpageDataTable extends LitElement {
.mainPage=${this.mainPage} .mainPage=${this.mainPage}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
> >
<div slot="toolbar-icon"><slot name="toolbar-icon"></slot></div> <div slot="toolbar-icon">
${this.narrow
? html`<div class="filter-menu">
${this.numHidden || this.activeFilters
? html`<span class="badge">${this.numHidden || "!"}</span>`
: ""}
<slot name="filter-menu"></slot>
</div>`
: ""}<slot name="toolbar-icon"></slot>
</div>
${this.narrow ${this.narrow
? html` ? html`
<div slot="header"> <div slot="header">
@@ -233,6 +242,10 @@ export class HaTabsSubpageDataTable extends LitElement {
`; `;
} }
private _preventDefault(ev) {
ev.preventDefault();
}
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {
if (this.filter === ev.detail.value) { if (this.filter === ev.detail.value) {
return; return;
@@ -267,6 +280,12 @@ export class HaTabsSubpageDataTable extends LitElement {
align-items: center; align-items: center;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
search-input {
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
--text-field-overflow: visible;
z-index: 5;
}
.table-header search-input { .table-header search-input {
display: block; display: block;
position: absolute; position: absolute;
@@ -276,16 +295,19 @@ export class HaTabsSubpageDataTable extends LitElement {
} }
.search-toolbar search-input { .search-toolbar search-input {
display: block; display: block;
width: 100%;
color: var(--secondary-text-color); color: var(--secondary-text-color);
--mdc-text-field-fill-color: transparant;
--mdc-text-field-idle-line-color: var(--divider-color);
--mdc-ripple-color: transparant; --mdc-ripple-color: transparant;
} }
.filters { .filters {
--mdc-text-field-fill-color: var(--input-fill-color);
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
--mdc-shape-small: 4px;
--text-field-overflow: initial;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
width: 100%;
margin-right: 8px; margin-right: 8px;
color: var(--primary-text-color);
} }
.active-filters { .active-filters {
color: var(--primary-text-color); color: var(--primary-text-color);
@@ -295,6 +317,8 @@ export class HaTabsSubpageDataTable extends LitElement {
padding: 2px 2px 2px 8px; padding: 2px 2px 2px 8px;
margin-left: 4px; margin-left: 4px;
font-size: 14px; font-size: 14px;
width: max-content;
cursor: initial;
} }
.active-filters ha-svg-icon { .active-filters ha-svg-icon {
color: var(--primary-color); color: var(--primary-color);
@@ -313,6 +337,24 @@ export class HaTabsSubpageDataTable extends LitElement {
left: 0; left: 0;
content: ""; content: "";
} }
.badge {
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 400;
background-color: var(--primary-color);
line-height: 20px;
text-align: center;
padding: 0px 4px;
color: var(--text-primary-color);
position: absolute;
right: 0;
top: 4px;
font-size: 0.65em;
}
.filter-menu {
position: relative;
}
`; `;
} }
} }

View File

@@ -272,6 +272,7 @@ class HassTabsSubpage extends LitElement {
ha-menu-button, ha-menu-button,
ha-icon-button-arrow-prev, ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) { ::slotted([slot="toolbar-icon"]) {
display: flex;
flex-shrink: 0; flex-shrink: 0;
pointer-events: auto; pointer-events: auto;
color: var(--sidebar-icon-color); color: var(--sidebar-icon-color);

View File

@@ -1,7 +1,6 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } 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";

View File

@@ -1,4 +1,5 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit"; import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -48,41 +49,35 @@ export class HaTriggerCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers" "ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
); );
} }
return html`<paper-dropdown-menu-light return html`<mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.id" "ui.panel.config.automation.editor.conditions.type.trigger.id"
)} )}
no-animations .value=${id}
@selected=${this._triggerPicked}
> >
<paper-listbox ${ensureArray(this._triggers).map((trigger) =>
slot="dropdown-content" trigger.id
.selected=${id} ? html`
attr-for-selected="data-trigger-id" <mwc-list-item .value=${trigger.id}>
@selected-item-changed=${this._triggerPicked} ${trigger.id}
> </mwc-list-item>
${ensureArray(this._triggers).map((trigger) => `
trigger.id : ""
? html` )}
<paper-item data-trigger-id=${trigger.id}> </mwc-select>`;
${trigger.id}
</paper-item>
`
: ""
)}
</paper-listbox>
</paper-dropdown-menu-light>`;
} }
private _automationUpdated(config?: AutomationConfig) { private _automationUpdated(config?: AutomationConfig) {
this._triggers = config?.trigger; this._triggers = config?.trigger;
} }
private _triggerPicked(ev: CustomEvent) { private _triggerPicked(ev) {
ev.stopPropagation(); ev.stopPropagation();
if (!ev.detail.value) { if (!ev.target.value) {
return; return;
} }
const newTrigger = ev.detail.value.dataset.triggerId; const newTrigger = ev.target.value;
if (this.condition.id === newTrigger) { if (this.condition.id === newTrigger) {
return; return;
} }

View File

@@ -1,15 +1,13 @@
import "@polymer/paper-input/paper-input"; import "../../../../../components/ha-form/ha-form";
import "@polymer/paper-input/paper-textarea";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import { createDurationData } from "../../../../../common/datetime/create_duration_data"; import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template"; import { hasTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/entity/ha-entity-picker"; import type { NumericStateTrigger } from "../../../../../data/automation";
import { NumericStateTrigger } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types";
import { HomeAssistant } from "../../../../../types";
import { handleChangeEvent } from "../ha-automation-trigger-row";
import "../../../../../components/ha-duration-input";
@customElement("ha-automation-trigger-numeric_state") @customElement("ha-automation-trigger-numeric_state")
export class HaNumericStateTrigger extends LitElement { export class HaNumericStateTrigger extends LitElement {
@@ -17,6 +15,22 @@ export class HaNumericStateTrigger extends LitElement {
@property() public trigger!: NumericStateTrigger; @property() public trigger!: NumericStateTrigger;
private _schema = memoizeOne((entityId): HaFormSchema[] => [
{ name: "entity_id", selector: { entity: {} } },
{
name: "attribute",
selector: { attribute: { entity_id: entityId } },
},
{ name: "above", required: false, selector: { text: {} } },
{ name: "below", required: false, selector: { text: {} } },
{
name: "value_template",
required: false,
selector: { text: { multiline: true } },
},
{ name: "for", required: false, selector: { duration: {} } },
]);
public willUpdate(changedProperties: PropertyValues) { public willUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("trigger")) { if (!changedProperties.has("trigger")) {
return; return;
@@ -38,67 +52,46 @@ export class HaNumericStateTrigger extends LitElement {
} }
public render() { public render() {
const { value_template, entity_id, attribute, below, above } = this.trigger;
const trgFor = createDurationData(this.trigger.for); const trgFor = createDurationData(this.trigger.for);
const data = { ...this.trigger, for: trgFor };
const schema = this._schema(this.trigger.entity_id);
return html` return html`
<ha-entity-picker <ha-form
.value=${entity_id}
@value-changed=${this._valueChanged}
.name=${"entity_id"}
.hass=${this.hass} .hass=${this.hass}
allow-custom-entity .data=${data}
></ha-entity-picker> .schema=${schema}
<ha-entity-attribute-picker
.hass=${this.hass}
.entityId=${entity_id}
.value=${attribute}
.name=${"attribute"}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.attribute"
)}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
allow-custom-value .computeLabel=${this._computeLabelCallback}
></ha-entity-attribute-picker> ></ha-form>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.above"
)}
name="above"
.value=${above}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.below"
)}
name="below"
.value=${below}
@value-changed=${this._valueChanged}
></paper-input>
<paper-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.value_template"
)}
name="value_template"
.value=${value_template}
@value-changed=${this._valueChanged}
dir="ltr"
></paper-textarea>
<ha-duration-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.state.for"
)}
.name=${"for"}
.data=${trgFor}
@value-changed=${this._valueChanged}
></ha-duration-input>
`; `;
} }
private _valueChanged(ev: CustomEvent): void { private _valueChanged(ev: CustomEvent): void {
handleChangeEvent(this, ev); ev.stopPropagation();
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
} }
private _computeLabelCallback = (schema: HaFormSchema): string => {
switch (schema.name) {
case "entity_id":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "attribute":
return this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
);
case "for":
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.state.for`
);
default:
return this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.numeric_state.${schema.name}`
);
}
};
} }
declare global { declare global {

View File

@@ -1,13 +1,14 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } 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 { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
import { TagTrigger } from "../../../../../data/automation"; import { TagTrigger } from "../../../../../data/automation";
import { fetchTags, Tag } from "../../../../../data/tag"; import { fetchTags, Tag } from "../../../../../data/tag";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import { TriggerElement } from "../ha-automation-trigger-row"; import { TriggerElement } from "../ha-automation-trigger-row";
import "../../../../../components/ha-paper-dropdown-menu";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
@customElement("ha-automation-trigger-tag") @customElement("ha-automation-trigger-tag")
export class HaTagTrigger extends LitElement implements TriggerElement { export class HaTagTrigger extends LitElement implements TriggerElement {
@@ -29,27 +30,22 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
protected render() { protected render() {
const { tag_id } = this.trigger; const { tag_id } = this.trigger;
return html` return html`
<ha-paper-dropdown-menu <mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.tag.label" "ui.panel.config.automation.editor.triggers.type.tag.label"
)} )}
?disabled=${this._tags.length === 0} .disabled=${this._tags.length === 0}
.value=${tag_id}
@selected=${this._tagChanged}
> >
<paper-listbox ${this._tags.map(
slot="dropdown-content" (tag) => html`
.selected=${tag_id} <mwc-list-item .value=${tag.id}>
attr-for-selected="tag_id" ${tag.name || tag.id}
@iron-select=${this._tagChanged} </mwc-list-item>
> `
${this._tags.map( )}
(tag) => html` </mwc-select>
<paper-item tag_id=${tag.id} .tag=${tag}>
${tag.name || tag.id}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
`; `;
} }
@@ -64,8 +60,14 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
...this.trigger, ...this.trigger,
tag_id: ev.detail.item.tag.id, tag_id: ev.target.value,
}, },
}); });
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-tag": HaTagTrigger;
}
}

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";

View File

@@ -5,18 +5,17 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch"; import "../../../../components/ha-switch";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
import { import {
CloudStatusLoggedIn,
CloudTTSInfo, CloudTTSInfo,
getCloudTTSInfo, getCloudTTSInfo,
updateCloudPref, getCloudTtsLanguages,
} from "../../../../data/cloud"; getCloudTtsSupportedGenders,
} from "../../../../data/cloud/tts";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import { translationMetadata } from "../../../../resources/translations-metadata";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showTryTtsDialog } from "./show-dialog-cloud-tts-try"; import { showTryTtsDialog } from "./show-dialog-cloud-tts-try";
@@ -37,7 +36,11 @@ export class CloudTTSPref extends LitElement {
const languages = this.getLanguages(this.ttsInfo); const languages = this.getLanguages(this.ttsInfo);
const defaultVoice = this.cloudStatus.prefs.tts_default_voice; const defaultVoice = this.cloudStatus.prefs.tts_default_voice;
const genders = this.getSupportedGenders(defaultVoice[0], this.ttsInfo); const genders = this.getSupportedGenders(
defaultVoice[0],
this.ttsInfo,
this.hass.localize
);
return html` return html`
<ha-card <ha-card
@@ -100,61 +103,9 @@ export class CloudTTSPref extends LitElement {
} }
} }
private getLanguages = memoizeOne((info?: CloudTTSInfo) => { private getLanguages = memoizeOne(getCloudTtsLanguages);
const languages: Array<[string, string]> = [];
if (!info) { private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders);
return languages;
}
const seen = new Set<string>();
for (const [lang] of info.languages) {
if (seen.has(lang)) {
continue;
}
seen.add(lang);
let label = lang;
if (lang in translationMetadata.translations) {
label = translationMetadata.translations[lang].nativeName;
} else {
const [langFamily, dialect] = lang.split("-");
if (langFamily in translationMetadata.translations) {
label = `${translationMetadata.translations[langFamily].nativeName}`;
if (langFamily.toLowerCase() !== dialect.toLowerCase()) {
label += ` (${dialect})`;
}
}
}
languages.push([lang, label]);
}
return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
});
private getSupportedGenders = memoizeOne(
(language: string, info?: CloudTTSInfo) => {
const genders: Array<[string, string]> = [];
if (!info) {
return genders;
}
for (const [curLang, gender] of info.languages) {
if (curLang === language) {
genders.push([
gender,
this.hass.localize(`ui.panel.config.cloud.account.tts.${gender}`) ||
gender,
]);
}
}
return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
}
);
private _openTryDialog() { private _openTryDialog() {
showTryTtsDialog(this, { showTryTtsDialog(this, {
@@ -170,7 +121,11 @@ export class CloudTTSPref extends LitElement {
const language = ev.target.value; const language = ev.target.value;
const curGender = this.cloudStatus!.prefs.tts_default_voice[1]; const curGender = this.cloudStatus!.prefs.tts_default_voice[1];
const genders = this.getSupportedGenders(language, this.ttsInfo); const genders = this.getSupportedGenders(
language,
this.ttsInfo,
this.hass.localize
);
const newGender = genders.find((item) => item[0] === curGender) const newGender = genders.find((item) => item[0] === curGender)
? curGender ? curGender
: genders[0][0]; : genders[0][0];

View File

@@ -1,5 +1,3 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded";

View File

@@ -266,9 +266,6 @@ export class CloudRegister extends LitElement {
a { a {
color: var(--primary-color); color: var(--primary-color);
} }
paper-item {
cursor: pointer;
}
h1 { h1 {
margin: 0; margin: 0;
} }

View File

@@ -71,6 +71,7 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
<ha-switch <ha-switch
.checked=${this._showDeserialized} .checked=${this._showDeserialized}
@change=${this._showDeserializedChanged} @change=${this._showDeserializedChanged}
dialogInitialFocus
> >
</ha-switch> </ha-switch>
</ha-formfield> </ha-formfield>

View File

@@ -10,8 +10,10 @@ import {
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { import {
fetchZwaveNodeStatus,
getZwaveJsIdentifiersFromDevice, getZwaveJsIdentifiersFromDevice,
ZWaveJSNodeIdentifiers, ZWaveJSNodeIdentifiers,
ZWaveJSNodeStatus,
} from "../../../../../../data/zwave_js"; } from "../../../../../../data/zwave_js";
import { haStyle } from "../../../../../../resources/styles"; import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types"; import { HomeAssistant } from "../../../../../../types";
@@ -29,43 +31,67 @@ export class HaDeviceActionsZWaveJS extends LitElement {
@state() private _nodeId?: number; @state() private _nodeId?: number;
@state() private _node?: ZWaveJSNodeStatus;
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) { if (changedProperties.has("device")) {
this._entryId = this.device.config_entries[0];
const identifiers: ZWaveJSNodeIdentifiers | undefined = const identifiers: ZWaveJSNodeIdentifiers | undefined =
getZwaveJsIdentifiersFromDevice(this.device); getZwaveJsIdentifiersFromDevice(this.device);
if (!identifiers) { if (!identifiers) {
return; return;
} }
this._nodeId = identifiers.node_id; this._nodeId = identifiers.node_id;
this._entryId = this.device.config_entries[0];
this._fetchNodeDetails();
} }
} }
protected async _fetchNodeDetails() {
if (!this._nodeId || !this._entryId) {
return;
}
this._node = await fetchZwaveNodeStatus(
this.hass,
this._entryId,
this._nodeId
);
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._node) {
return html``;
}
return html` return html`
<a ${!this._node.is_controller_node
.href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`} ? html`
> <a
<mwc-button> .href=${`/config/zwave_js/node_config/${this.device.id}?config_entry=${this._entryId}`}
${this.hass.localize( >
"ui.panel.config.zwave_js.device_info.device_config" <mwc-button>
)} ${this.hass.localize(
</mwc-button> "ui.panel.config.zwave_js.device_info.device_config"
</a> )}
<mwc-button @click=${this._reinterviewClicked}> </mwc-button>
${this.hass.localize( </a>
"ui.panel.config.zwave_js.device_info.reinterview_device" <mwc-button @click=${this._reinterviewClicked}>
)} ${this.hass.localize(
</mwc-button> "ui.panel.config.zwave_js.device_info.reinterview_device"
<mwc-button @click=${this._healNodeClicked}> )}
${this.hass.localize("ui.panel.config.zwave_js.device_info.heal_node")} </mwc-button>
</mwc-button> <mwc-button @click=${this._healNodeClicked}>
<mwc-button @click=${this._removeFailedNode}> ${this.hass.localize(
${this.hass.localize( "ui.panel.config.zwave_js.device_info.heal_node"
"ui.panel.config.zwave_js.device_info.remove_failed" )}
)} </mwc-button>
</mwc-button> <mwc-button @click=${this._removeFailedNode}>
${this.hass.localize(
"ui.panel.config.zwave_js.device_info.remove_failed"
)}
</mwc-button>
`
: ""}
`; `;
} }

View File

@@ -103,52 +103,58 @@ export class HaDeviceInfoZWaveJS extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}: ${this.hass.localize("ui.panel.config.zwave_js.common.node_id")}:
${this._node.node_id} ${this._node.node_id}
</div> </div>
<div> ${!this._node.is_controller_node
${this.hass.localize( ? html`
"ui.panel.config.zwave_js.device_info.node_status" <div>
)}: ${this.hass.localize(
${this.hass.localize( "ui.panel.config.zwave_js.device_info.node_status"
`ui.panel.config.zwave_js.node_status.${ )}:
nodeStatus[this._node.status] ${this.hass.localize(
}` `ui.panel.config.zwave_js.node_status.${
)} nodeStatus[this._node.status]
</div> }`
<div> )}
${this.hass.localize( </div>
"ui.panel.config.zwave_js.device_info.node_ready" <div>
)}: ${this.hass.localize(
${this._node.ready "ui.panel.config.zwave_js.device_info.node_ready"
? this.hass.localize("ui.common.yes") )}:
: this.hass.localize("ui.common.no")} ${this._node.ready
</div> ? this.hass.localize("ui.common.yes")
<div> : this.hass.localize("ui.common.no")}
${this.hass.localize( </div>
"ui.panel.config.zwave_js.device_info.highest_security" <div>
)}: ${this.hass.localize(
${this._node.highest_security_class !== null "ui.panel.config.zwave_js.device_info.highest_security"
? this.hass.localize( )}:
`ui.panel.config.zwave_js.security_classes.${ ${this._node.highest_security_class !== null
SecurityClass[this._node.highest_security_class] ? this.hass.localize(
}.title` `ui.panel.config.zwave_js.security_classes.${
) SecurityClass[this._node.highest_security_class]
: this._node.is_secure === false }.title`
? this.hass.localize( )
"ui.panel.config.zwave_js.security_classes.none.title" : this._node.is_secure === false
) ? this.hass.localize(
: this.hass.localize("ui.panel.config.zwave_js.device_info.unknown")} "ui.panel.config.zwave_js.security_classes.none.title"
</div> )
<div> : this.hass.localize(
${this.hass.localize( "ui.panel.config.zwave_js.device_info.unknown"
"ui.panel.config.zwave_js.device_info.zwave_plus" )}
)}: </div>
${this._node.zwave_plus_version <div>
? this.hass.localize( ${this.hass.localize(
"ui.panel.config.zwave_js.device_info.zwave_plus_version", "ui.panel.config.zwave_js.device_info.zwave_plus"
"version", )}:
this._node.zwave_plus_version ${this._node.zwave_plus_version
) ? this.hass.localize(
: this.hass.localize("ui.common.no")} "ui.panel.config.zwave_js.device_info.zwave_plus_version",
</div> "version",
this._node.zwave_plus_version
)
: this.hass.localize("ui.common.no")}
</div>
`
: ""}
`; `;
} }

View File

@@ -1,6 +1,7 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperItemElement } from "@polymer/paper-item/paper-item";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
@@ -17,7 +18,6 @@ import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-area-picker"; import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel"; import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch";
import { import {
@@ -158,28 +158,23 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
></ha-icon-picker> ></ha-icon-picker>
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) || ${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
(domain === "cover" && this.entry.original_device_class === null) (domain === "cover" && this.entry.original_device_class === null)
? html`<ha-paper-dropdown-menu ? html`<mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class" "ui.dialogs.entity_registry.editor.device_class"
)} )}
.value=${this._deviceClass}
@selected=${this._deviceClassChanged}
> >
<paper-listbox ${OVERRIDE_DEVICE_CLASSES[domain].map(
slot="dropdown-content" (deviceClass: string) => html`
attr-for-selected="item-value" <mwc-list-item .value=${deviceClass}>
.selected=${this._deviceClass} ${this.hass.localize(
@selected-item-changed=${this._deviceClassChanged} `ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
> )}
${OVERRIDE_DEVICE_CLASSES[domain].map( </mwc-list-item>
(deviceClass: string) => html` `
<paper-item .itemValue=${deviceClass}> )}
${this.hass.localize( </mwc-select>`
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>`
: ""} : ""}
<paper-input <paper-input
.value=${this._entityId} .value=${this._entityId}
@@ -302,12 +297,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._entityId = ev.detail.value; this._entityId = ev.detail.value;
} }
private _deviceClassChanged(ev: PolymerChangedEvent<PaperItemElement>): void { private _deviceClassChanged(ev): void {
this._error = undefined; this._error = undefined;
if (ev.detail.value === null) { this._deviceClass = ev.target.value;
return;
}
this._deviceClass = (ev.detail.value as any).itemValue;
} }
private _areaPicked(ev: CustomEvent) { private _areaPicked(ev: CustomEvent) {
@@ -425,7 +417,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
padding-bottom: max(env(safe-area-inset-bottom), 8px); padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff); background-color: var(--mdc-theme-surface, #fff);
} }
ha-paper-dropdown-menu { mwc-select {
width: 100%; width: 100%;
} }
ha-switch { ha-switch {

View File

@@ -22,10 +22,6 @@ documentContainer.innerHTML = `<dom-module id="ha-form-style">
@apply --layout-vertical; @apply --layout-vertical;
@apply --layout-start; @apply --layout-start;
} }
paper-dropdown-menu.form-control {
margin: -9px 0;
}
</style> </style>
</template> </template>
</dom-module>`; </dom-module>`;

View File

@@ -21,8 +21,6 @@ import {
mdiTools, mdiTools,
mdiViewDashboard, mdiViewDashboard,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { PolymerElement } from "@polymer/polymer"; import { PolymerElement } from "@polymer/polymer";
import { PropertyValues } from "lit"; import { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } 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 { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive";

View File

@@ -297,29 +297,36 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._filter this._filter
); );
const filterMenu = html`<ha-button-menu const filterMenu = html`<div
corner="BOTTOM_START" slot=${ifDefined(this.narrow ? "toolbar-icon" : "suffix")}
multi
slot=${ifDefined(this.narrow ? "toolbar-icon" : undefined)}
@action=${this._handleMenuAction}
> >
<ha-icon-button ${!this._showDisabled && this.narrow && disabledCount
slot="trigger" ? html`<span class="badge">${disabledCount}</span>`
.label=${this.hass.localize("ui.common.menu")} : ""}
.path=${mdiFilterVariant} <ha-button-menu
corner="BOTTOM_START"
multi
@action=${this._handleMenuAction}
@click=${this._preventDefault}
> >
</ha-icon-button> <ha-icon-button
<ha-check-list-item left .selected=${this._showIgnored}> slot="trigger"
${this.hass.localize( .label=${this.hass.localize("ui.common.menu")}
"ui.panel.config.integrations.ignore.show_ignored" .path=${mdiFilterVariant}
)} >
</ha-check-list-item> </ha-icon-button>
<ha-check-list-item left .selected=${this._showDisabled}> <ha-check-list-item left .selected=${this._showIgnored}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.integrations.disable.show_disabled" "ui.panel.config.integrations.ignore.show_ignored"
)} )}
</ha-check-list-item> </ha-check-list-item>
</ha-button-menu>`; <ha-check-list-item left .selected=${this._showDisabled}>
${this.hass.localize(
"ui.panel.config.integrations.disable.show_disabled"
)}
</ha-check-list-item>
</ha-button-menu>
</div>`;
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@@ -336,8 +343,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.hass=${this.hass} .hass=${this.hass}
.filter=${this._filter} .filter=${this._filter}
class="header" class="header"
no-label-float
no-underline
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.integrations.search" "ui.panel.config.integrations.search"
@@ -350,29 +355,33 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
<div class="search"> <div class="search">
<search-input <search-input
.hass=${this.hass} .hass=${this.hass}
no-label-float suffix
no-underline
.filter=${this._filter} .filter=${this._filter}
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.integrations.search" "ui.panel.config.integrations.search"
)} )}
></search-input> >
${!this._showDisabled && disabledCount ${!this._showDisabled && disabledCount
? html`<div class="active-filters"> ? html`<div
${this.hass.localize( class="active-filters"
"ui.panel.config.integrations.disable.disabled_integrations", slot="suffix"
{ number: disabledCount } @click=${this._preventDefault}
)} >
<mwc-button ${this.hass.localize(
@click=${this._toggleShowDisabled} "ui.panel.config.integrations.disable.disabled_integrations",
.label=${this.hass.localize( { number: disabledCount }
"ui.panel.config.integrations.disable.show"
)} )}
></mwc-button> <mwc-button
</div>` @click=${this._toggleShowDisabled}
: ""} .label=${this.hass.localize(
${filterMenu} "ui.panel.config.integrations.disable.show"
)}
></mwc-button>
</div>`
: ""}
${filterMenu}
</search-input>
</div> </div>
`} `}
@@ -503,6 +512,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
`; `;
} }
private _preventDefault(ev) {
ev.preventDefault();
}
private _loadConfigEntries() { private _loadConfigEntries() {
getConfigEntries(this.hass).then((configEntries) => { getConfigEntries(this.hass).then((configEntries) => {
this._configEntries = configEntries this._configEntries = configEntries
@@ -683,13 +696,15 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.empty-message h1 { .empty-message h1 {
margin-bottom: 0; margin-bottom: 0;
} }
search-input {
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
--text-field-overflow: visible;
}
search-input.header { search-input.header {
display: block; display: block;
color: var(--secondary-text-color); color: var(--secondary-text-color);
margin-left: 8px; margin-left: 8px;
--mdc-text-field-fill-color: transparant;
--mdc-text-field-idle-line-color: var(--divider-color);
--mdc-ripple-color: transparant; --mdc-ripple-color: transparant;
} }
.search { .search {
@@ -717,6 +732,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
align-items: center; align-items: center;
padding: 2px 2px 2px 8px; padding: 2px 2px 2px 8px;
font-size: 14px; font-size: 14px;
width: max-content;
cursor: initial;
} }
.active-filters mwc-button { .active-filters mwc-button {
margin-left: 8px; margin-left: 8px;
@@ -732,6 +749,24 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
left: 0; left: 0;
content: ""; content: "";
} }
.badge {
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 400;
background-color: var(--primary-color);
line-height: 20px;
text-align: center;
padding: 0px 4px;
color: var(--text-primary-color);
position: absolute;
right: 14px;
top: 8px;
font-size: 0.65em;
}
ha-button-menu {
color: var(--primary-text-color);
}
`, `,
]; ];
} }

View File

@@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiFolderMultipleOutline, mdiLan, mdiNetwork, mdiPlus } from "@mdi/js"; import { mdiFolderMultipleOutline, mdiLan, mdiNetwork, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,

View File

@@ -1,6 +1,6 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { mdiDownload } from "@mdi/js"; import { mdiDownload } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultArray, html, LitElement } from "lit"; import { css, CSSResultArray, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
@@ -77,26 +77,20 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
<div class="card-content"> <div class="card-content">
${this._logConfig ${this._logConfig
? html` ? html`
<paper-dropdown-menu <mwc-select
dynamic-align
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.zwave_js.logs.log_level" "ui.panel.config.zwave_js.logs.log_level"
)} )}
.value=${this._logConfig.level}
@selected=${this._dropdownSelected}
> >
<paper-listbox <mwc-list-item value="error">Error</mwc-list-item>
slot="dropdown-content" <mwc-list-item value="warn">Warn</mwc-list-item>
.selected=${this._logConfig.level} <mwc-list-item value="info">Info</mwc-list-item>
attr-for-selected="value" <mwc-list-item value="verbose">Verbose</mwc-list-item>
@iron-select=${this._dropdownSelected} <mwc-list-item value="debug">Debug</mwc-list-item>
> <mwc-list-item value="silly">Silly</mwc-list-item>
<paper-item value="error">Error</paper-item> </mwc-select>
<paper-item value="warn">Warn</paper-item>
<paper-item value="info">Info</paper-item>
<paper-item value="verbose">Verbose</paper-item>
<paper-item value="debug">Debug</paper-item>
<paper-item value="silly">Silly</paper-item>
</paper-listbox>
</paper-dropdown-menu>
` `
: ""} : ""}
</div> </div>
@@ -142,7 +136,7 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) {
if (ev.target === undefined || this._logConfig === undefined) { if (ev.target === undefined || this._logConfig === undefined) {
return; return;
} }
const selected = ev.target.selected; const selected = ev.target.value;
if (this._logConfig.level === selected) { if (this._logConfig.level === selected) {
return; return;
} }

View File

@@ -1,13 +1,12 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { import {
mdiCheckCircle, mdiCheckCircle,
mdiCircle, mdiCircle,
mdiCloseCircle, mdiCloseCircle,
mdiProgressClock, mdiProgressClock,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
@@ -287,26 +286,20 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
return html` return html`
${labelAndDescription} ${labelAndDescription}
<div class="flex"> <div class="flex">
<paper-dropdown-menu <mwc-select
dynamic-align
.disabled=${!item.metadata.writeable} .disabled=${!item.metadata.writeable}
.value=${item.value}
.key=${id}
.property=${item.property}
.propertyKey=${item.property_key}
@selected=${this._dropdownSelected}
> >
<paper-listbox ${Object.entries(item.metadata.states).map(
slot="dropdown-content" ([key, entityState]) => html`
.selected=${item.value} <mwc-list-item .value=${key}>${entityState}</mwc-list-item>
attr-for-selected="value" `
.key=${id} )}
.property=${item.property} </mwc-select>
.propertyKey=${item.property_key}
@iron-select=${this._dropdownSelected}
>
${Object.entries(item.metadata.states).map(
([key, entityState]) => html`
<paper-item .value=${key}>${entityState}</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
`; `;
} }
@@ -351,12 +344,12 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
if (ev.target === undefined || this._config![ev.target.key] === undefined) { if (ev.target === undefined || this._config![ev.target.key] === undefined) {
return; return;
} }
if (this._config![ev.target.key].value === ev.target.selected) { if (this._config![ev.target.key].value === ev.target.value) {
return; return;
} }
this.setResult(ev.target.key, undefined); this.setResult(ev.target.key, undefined);
this._updateConfigParameter(ev.target, Number(ev.target.selected)); this._updateConfigParameter(ev.target, Number(ev.target.value));
} }
private debouncedUpdate = debounce((target, value) => { private debouncedUpdate = debounce((target, value) => {
@@ -462,7 +455,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
} }
.flex .config-label, .flex .config-label,
.flex paper-dropdown-menu { .flex mwc-select {
flex: 1; flex: 1;
} }

View File

@@ -104,7 +104,7 @@ class DialogSystemLogDetail extends LitElement {
)} )}
</ha-alert>` </ha-alert>`
: ""} : ""}
<div class="contents"> <div class="contents" tabindex="-1" dialogInitialFocus>
<p> <p>
Logger: ${item.name}<br /> Logger: ${item.name}<br />
Source: ${item.source.join(":")} Source: ${item.source.join(":")}
@@ -227,6 +227,7 @@ class DialogSystemLogDetail extends LitElement {
} }
.contents { .contents {
padding: 16px; padding: 16px;
outline: none;
} }
.error { .error {
color: var(--error-color); color: var(--error-color);

View File

@@ -105,10 +105,10 @@ export class HaConfigLogs extends LitElement {
} }
search-input { search-input {
display: block; display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
} }
search-input.header { search-input.header {
--mdc-text-field-fill-color: transparant;
--mdc-text-field-idle-line-color: var(--divider-color);
--mdc-ripple-color: transparant; --mdc-ripple-color: transparant;
} }
.content { .content {

View File

@@ -1,11 +1,8 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { createCloseHeading } from "../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-paper-dropdown-menu";
import { import {
LovelaceResource, LovelaceResource,
LovelaceResourcesMutableParams, LovelaceResourcesMutableParams,
@@ -14,6 +11,9 @@ import { PolymerChangedEvent } from "../../../../polymer-types";
import { haStyleDialog } from "../../../../resources/styles"; import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail"; import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
const detectResourceType = (url: string) => { const detectResourceType = (url: string) => {
const ext = url.split(".").pop() || ""; const ext = url.split(".").pop() || "";
@@ -102,48 +102,44 @@ export class DialogLovelaceResourceDetail extends LitElement {
dialogInitialFocus dialogInitialFocus
></paper-input> ></paper-input>
<br /> <br />
<ha-paper-dropdown-menu <mwc-select
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.config.lovelace.resources.detail.type" "ui.panel.config.lovelace.resources.detail.type"
)} )}
.value=${this._type}
@selected=${this._typeChanged}
@closed=${stopPropagation}
.invalid=${!this._type}
> >
<paper-listbox <mwc-list-item value="module">
slot="dropdown-content" ${this.hass!.localize(
.selected=${this._type} "ui.panel.config.lovelace.resources.types.module"
@iron-select=${this._typeChanged} )}
attr-for-selected="type" </mwc-list-item>
.invalid=${!this._type} ${this._type === "js"
> ? html`
<paper-item type="module"> <mwc-list-item value="js">
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.lovelace.resources.types.module" "ui.panel.config.lovelace.resources.types.js"
)} )}
</paper-item> </mwc-list-item>
${this._type === "js" `
? html` : ""}
<paper-item type="js"> <mwc-list-item value="css">
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.lovelace.resources.types.js" "ui.panel.config.lovelace.resources.types.css"
)} )}
</paper-item> </mwc-list-item>
` ${this._type === "html"
: ""} ? html`
<paper-item type="css"> <mwc-list-item value="html">
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.config.lovelace.resources.types.css" "ui.panel.config.lovelace.resources.types.html"
)} )}
</paper-item> </mwc-list-item>
${this._type === "html" `
? html` : ""}
<paper-item type="html"> </mwc-select>
${this.hass!.localize(
"ui.panel.config.lovelace.resources.types.html"
)}
</paper-item>
`
: ""}
</paper-listbox>
</ha-paper-dropdown-menu>
</div> </div>
</div> </div>
${this._params.resource ${this._params.resource
@@ -185,8 +181,8 @@ export class DialogLovelaceResourceDetail extends LitElement {
} }
} }
private _typeChanged(ev: CustomEvent) { private _typeChanged(ev) {
this._type = ev.detail.item.getAttribute("type"); this._type = ev.target.value;
} }
private async _updateResource() { private async _updateResource() {

View File

@@ -1,6 +1,4 @@
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-icon-item";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one"; import memoize from "memoize-one";

View File

@@ -1,4 +1,4 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { import {
mdiCheck, mdiCheck,
@@ -9,8 +9,6 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import { PaperListboxElement } from "@polymer/paper-listbox";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -21,6 +19,7 @@ import {
} from "lit"; } from "lit";
import { property, state, query } from "lit/decorators"; import { property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { computeObjectId } from "../../../common/entity/compute_object_id"; import { computeObjectId } from "../../../common/entity/compute_object_id";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify"; import { slugify } from "../../../common/string/slugify";
@@ -29,8 +28,12 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import type {
HaFormDataContainer,
HaFormSchema,
HaFormSelector,
} from "../../../components/ha-form/types";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-picker";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor"; import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
@@ -49,10 +52,9 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
import "../../../layouts/ha-app-layout"; import "../../../layouts/ha-app-layout";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import type { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import "../automation/action/ha-automation-action";
import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@@ -83,7 +85,91 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@query("ha-yaml-editor", true) private _editor?: HaYamlEditor; @query("ha-yaml-editor", true) private _editor?: HaYamlEditor;
private _schema = memoizeOne(
(hasID: boolean, useBluePrint?: boolean, currentMode?: string) => {
const schema: HaFormSchema[] = [
{
name: "alias",
selector: {
text: {
type: "text",
},
},
},
{
name: "icon",
selector: {
icon: {},
},
},
];
if (!hasID) {
schema.push({
name: "id",
selector: {
text: {},
},
});
}
if (!useBluePrint) {
schema.push({
name: "mode",
selector: {
select: {
options: MODES.map((mode) => ({
label: `
${
this.hass.localize(
`ui.panel.config.script.editor.modes.${mode}`
) || mode
}
`,
value: mode,
})),
},
},
});
}
if (currentMode && MODES_MAX.includes(currentMode)) {
schema.push({
name: "max",
selector: {
text: {
type: "number",
},
},
});
}
return schema;
}
);
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._config) {
return html``;
}
const schema = this._schema(
!!this.scriptEntityId,
"use_blueprint" in this._config,
this._config.mode
);
const data = {
mode: MODES[0],
max:
this._config.mode && MODES_MAX.includes(this._config.mode)
? 10
: undefined,
icon: undefined,
...this._config,
id: this._entityId,
};
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
@@ -113,11 +199,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
> >
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${this._mode === "gui" ${this._mode === "gui"
? html` <ha-svg-icon ? html`
class="selected_menu_item" <ha-svg-icon
slot="graphic" class="selected_menu_item"
.path=${mdiCheck} slot="graphic"
></ha-svg-icon>` .path=${mdiCheck}
></ha-svg-icon>
`
: ``} : ``}
</mwc-list-item> </mwc-list-item>
<mwc-list-item <mwc-list-item
@@ -129,11 +217,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
> >
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")} ${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
${this._mode === "yaml" ${this._mode === "yaml"
? html` <ha-svg-icon ? html`
class="selected_menu_item" <ha-svg-icon
slot="graphic" class="selected_menu_item"
.path=${mdiCheck} slot="graphic"
></ha-svg-icon>` .path=${mdiCheck}
></ha-svg-icon>
`
: ``} : ``}
</mwc-list-item> </mwc-list-item>
@@ -173,16 +263,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
${this.narrow ${this.narrow
? html` <span slot="header">${this._config?.alias}</span> ` ? html`<span slot="header">${this._config?.alias}</span>`
: ""} : ""}
<div <div
class="content ${classMap({ class="content ${classMap({
"yaml-mode": this._mode === "yaml", "yaml-mode": this._mode === "yaml",
})}" })}"
> >
${this._errors ${this._errors ? html`<div class="errors">${this._errors}</div>` : ""}
? html` <div class="errors">${this._errors}</div> `
: ""}
${this._mode === "gui" ${this._mode === "gui"
? html` ? html`
<div <div
@@ -205,95 +293,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</span> </span>
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
<paper-input <ha-form
.label=${this.hass.localize( .schema=${schema}
"ui.panel.config.script.editor.alias" .data=${data}
)} .hass=${this.hass}
name="alias" .computeLabel=${this._computeLabelCallback}
.value=${this._config.alias} .computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@change=${this._aliasChanged} ></ha-form>
>
</paper-input>
<ha-icon-picker
.label=${this.hass.localize(
"ui.panel.config.script.editor.icon"
)}
.name=${"icon"}
.value=${this._config.icon}
@value-changed=${this._valueChanged}
>
</ha-icon-picker>
${!this.scriptEntityId
? html`<paper-input
.label=${this.hass.localize(
"ui.panel.config.script.editor.id"
)}
.errorMessage=${this.hass.localize(
"ui.panel.config.script.editor.id_already_exists"
)}
.invalid=${this._idError}
.value=${this._entityId}
@value-changed=${this._idChanged}
>
</paper-input>`
: ""}
${"use_blueprint" in this._config
? ""
: html`<p>
${this.hass.localize(
"ui.panel.config.script.editor.modes.description",
"documentation_link",
html`<a
href=${documentationUrl(
this.hass,
"/integrations/script/#script-modes"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.script.editor.modes.documentation"
)}</a
>`
)}
</p>
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.script.editor.modes.label"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${this._config.mode
? MODES.indexOf(this._config.mode)
: 0}
@iron-select=${this._modeChanged}
>
${MODES.map(
(mode) => html`
<paper-item .mode=${mode}>
${this.hass.localize(
`ui.panel.config.script.editor.modes.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._config.mode &&
MODES_MAX.includes(this._config.mode)
? html`<paper-input
.label=${this.hass.localize(
`ui.panel.config.script.editor.max.${this._config.mode}`
)}
type="number"
name="max"
.value=${this._config.max || "10"}
@value-changed=${this._valueChanged}
>
</paper-input>`
: html``} `}
</div> </div>
${this.scriptEntityId ${this.scriptEntityId
? html` ? html`
@@ -328,47 +335,51 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</ha-config-section> </ha-config-section>
${"use_blueprint" in this._config ${"use_blueprint" in this._config
? html`<blueprint-script-editor ? html`
.hass=${this.hass} <blueprint-script-editor
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
@value-changed=${this._configChanged}
></blueprint-script-editor>`
: html`<ha-config-section
vertical
.isWide=${this.isWide}
>
<span slot="header">
${this.hass.localize(
"ui.panel.config.script.editor.sequence"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.script.editor.sequence_sentence"
)}
</p>
<a
href=${documentationUrl(
this.hass,
"/docs/scripts/"
)}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.sequence}
@value-changed=${this._sequenceChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action> .narrow=${this.narrow}
</ha-config-section>`} .isWide=${this.isWide}
.config=${this._config}
@value-changed=${this._configChanged}
></blueprint-script-editor>
`
: html`
<ha-config-section
vertical
.isWide=${this.isWide}
>
<span slot="header">
${this.hass.localize(
"ui.panel.config.script.editor.sequence"
)}
</span>
<span slot="introduction">
<p>
${this.hass.localize(
"ui.panel.config.script.editor.sequence_sentence"
)}
</p>
<a
href=${documentationUrl(
this.hass,
"/docs/scripts/"
)}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
</a>
</span>
<ha-automation-action
.actions=${this._config.sequence}
@value-changed=${this._sequenceChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>
`}
` `
: ""} : ""}
</div> </div>
@@ -495,7 +506,50 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
} }
} }
private async _runScript(ev) { private _computeLabelCallback = (
schema: HaFormSelector,
data: HaFormDataContainer
): string => {
switch (schema.name) {
case "mode":
return this.hass.localize("ui.panel.config.script.editor.modes.label");
case "max":
return this.hass.localize(
`ui.panel.config.script.editor.max.${data.mode}`
);
default:
return this.hass.localize(
`ui.panel.config.script.editor.${schema.name}`
);
}
};
private _computeHelperCallback = (
schema: HaFormSelector
): string | undefined => {
if (schema.name === "mode") {
return this.hass.localize(
"ui.panel.config.script.editor.modes.description",
"documentation_link",
html`
<a
href=${documentationUrl(
this.hass,
"/integrations/script/#script-modes"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.script.editor.modes.documentation"
)}</a
>
`
);
}
return undefined;
};
private async _runScript(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
await triggerScript(this.hass, this.scriptEntityId as string); await triggerScript(this.hass, this.scriptEntityId as string);
showToast(this, { showToast(this, {
@@ -507,14 +561,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
}); });
} }
private _modeChanged(ev: CustomEvent) { private _modeChanged(mode) {
const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.mode;
if (mode === this._config!.mode) {
return;
}
this._config = { ...this._config!, mode }; this._config = { ...this._config!, mode };
if (!MODES_MAX.includes(mode)) { if (!MODES_MAX.includes(mode)) {
delete this._config.max; delete this._config.max;
@@ -522,23 +569,23 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
this._dirty = true; this._dirty = true;
} }
private _aliasChanged(ev: CustomEvent) { private _aliasChanged(alias: string) {
if (this.scriptEntityId || this._entityId) { if (this.scriptEntityId || this._entityId) {
return; return;
} }
const aliasSlugify = slugify((ev.target as any).value); const aliasSlugify = slugify(alias);
let id = aliasSlugify; let id = aliasSlugify;
let i = 2; let i = 2;
while (this.hass.states[`script.${id}`]) { while (this.hass.states[`script.${id}`]) {
id = `${aliasSlugify}_${i}`; id = `${aliasSlugify}_${i}`;
i++; i++;
} }
this._entityId = id; this._entityId = id;
} }
private _idChanged(ev: CustomEvent) { private _idChanged(id: string) {
ev.stopPropagation(); this._entityId = id;
this._entityId = (ev.target as any).value;
if (this.hass.states[`script.${this._entityId}`]) { if (this.hass.states[`script.${this._entityId}`]) {
this._idError = true; this._idError = true;
} else { } else {
@@ -548,24 +595,39 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const target = ev.target as any; const values = ev.detail.value as any;
const name = target.name;
if (!name) { for (const key of Object.keys(values)) {
return; if (key === "sequence") {
} continue;
let newVal = ev.detail.value; }
if (target.type === "number") {
newVal = Number(newVal); const value = values[key];
}
if ((this._config![name] || "") === newVal) { if (value === this._config![key]) {
return; continue;
} }
if (!newVal) {
delete this._config![name]; switch (key) {
this._config = { ...this._config! }; case "id":
} else { this._idChanged(value);
this._config = { ...this._config!, [name]: newVal }; return;
case "alias":
this._aliasChanged(value);
break;
case "mode":
this._modeChanged(value);
return;
}
if (values[key] === undefined) {
delete this._config![key];
this._config = { ...this._config! };
} else {
this._config = { ...this._config!, [key]: value };
}
} }
this._dirty = true; this._dirty = true;
} }
@@ -575,7 +637,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
} }
private _sequenceChanged(ev: CustomEvent): void { private _sequenceChanged(ev: CustomEvent): void {
this._config = { ...this._config!, sequence: ev.detail.value as Action[] }; this._config = {
...this._config!,
sequence: ev.detail.value as Action[],
};
this._errors = undefined; this._errors = undefined;
this._dirty = true; this._dirty = true;
} }

View File

@@ -111,6 +111,7 @@ export class DialogAddUser extends LitElement {
.errorMessage=${this.hass.localize("ui.common.error_required")} .errorMessage=${this.hass.localize("ui.common.error_required")}
@value-changed=${this._handleValueChanged} @value-changed=${this._handleValueChanged}
@blur=${this._maybePopulateUsername} @blur=${this._maybePopulateUsername}
dialogInitialFocus
></paper-input>` ></paper-input>`
: ""} : ""}
<paper-input <paper-input
@@ -125,6 +126,7 @@ export class DialogAddUser extends LitElement {
autocapitalize="none" autocapitalize="none"
@value-changed=${this._handleValueChanged} @value-changed=${this._handleValueChanged}
.errorMessage=${this.hass.localize("ui.common.error_required")} .errorMessage=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
></paper-input> ></paper-input>
<paper-input <paper-input

View File

@@ -65,6 +65,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
name="action" name="action"
.checked=${this._action === "update"} .checked=${this._action === "update"}
@change=${this._handleActionChanged} @change=${this._handleActionChanged}
dialogInitialFocus
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield

View File

@@ -47,7 +47,11 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
${this._params.issue.data.supported_unit}? ${this._params.issue.data.supported_unit}?
</p> </p>
<mwc-button slot="primaryAction" @click=${this._fixIssue}> <mwc-button
slot="primaryAction"
@click=${this._fixIssue}
dialogInitialFocus
>
Fix Fix
</mwc-button> </mwc-button>
<mwc-button slot="secondaryAction" @click=${this.closeDialog}> <mwc-button slot="secondaryAction" @click=${this.closeDialog}>

View File

@@ -1,11 +1,11 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { array, assert, assign, object, optional, string } from "superstruct"; import { array, assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@@ -109,18 +109,20 @@ export class HuiAlarmPanelCardEditor
</div> </div>
` `
)} )}
<paper-dropdown-menu <mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.lovelace.editor.card.alarm-panel.available_states" "ui.panel.lovelace.editor.card.alarm-panel.available_states"
)} )}
@value-changed=${this._stateAdded} @selected=${this._stateAdded}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
> >
<paper-listbox slot="dropdown-content"> ${states.map(
${states.map( (entityState) =>
(entityState) => html` <paper-item>${entityState}</paper-item> ` html`<mwc-list-item>${entityState}</mwc-list-item> `
)} )}
</paper-listbox> </mwc-select>
</paper-dropdown-menu>
<hui-theme-select-editor <hui-theme-select-editor
.hass=${this.hass} .hass=${this.hass}
.value=${this._theme} .value=${this._theme}

View File

@@ -1,3 +1,5 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { import {
@@ -11,15 +13,16 @@ import {
union, union,
} from "superstruct"; } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entities-picker"; import "../../../../components/entity/ha-entities-picker";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { CalendarCardConfig } from "../../cards/types"; import type { CalendarCardConfig } from "../../cards/types";
import "../../components/hui-entity-editor"; import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor"; import "../../components/hui-theme-select-editor";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { EditorTarget, EntitiesEditorEvent } from "../types"; import type { EditorTarget, EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@@ -80,29 +83,25 @@ export class HuiCalendarCardEditor
.configValue=${"title"} .configValue=${"title"}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
<paper-dropdown-menu <mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.lovelace.editor.card.calendar.inital_view" "ui.panel.lovelace.editor.card.calendar.inital_view"
)} )}
.value=${this._initial_view}
.configValue=${"initial_view"}
@selected=${this._viewChanged}
@closed=${stopPropagation}
> >
<paper-listbox ${views.map(
slot="dropdown-content" (view) => html`
attr-for-selected="view" <mwc-list-item .value=${view}
.selected=${this._initial_view} >${this.hass!.localize(
.configValue=${"initial_view"} `ui.panel.lovelace.editor.card.calendar.views.${view}`
@iron-select=${this._viewChanged} )}
> </mwc-list-item>
${views.map( `
(view) => html` )}
<paper-item .view=${view} </mwc-select>
>${this.hass!.localize(
`ui.panel.lovelace.editor.card.calendar.views.${view}`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
<hui-theme-select-editor <hui-theme-select-editor
.hass=${this.hass} .hass=${this.hass}
@@ -157,18 +156,18 @@ export class HuiCalendarCardEditor
fireEvent(this, "config-changed", { config: this._config }); fireEvent(this, "config-changed", { config: this._config });
} }
private _viewChanged(ev: CustomEvent): void { private _viewChanged(ev): void {
if (!this._config || !this.hass) { if (!this._config || !this.hass) {
return; return;
} }
if (ev.detail.item.view === "") { if (ev.target.value === "") {
this._config = { ...this._config }; this._config = { ...this._config };
delete this._config.initial_view; delete this._config.initial_view;
} else { } else {
this._config = { this._config = {
...this._config, ...this._config,
initial_view: ev.detail.item.view, initial_view: ev.target.value,
}; };
} }
fireEvent(this, "config-changed", { config: this._config }); fireEvent(this, "config-changed", { config: this._config });

View File

@@ -1,8 +1,10 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@material/mwc-tab-bar/mwc-tab-bar"; import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab"; import "@material/mwc-tab/mwc-tab";
import type { MDCTabBarActivatedEvent } from "@material/tab-bar"; import type { MDCTabBarActivatedEvent } from "@material/tab-bar";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { import {
any, any,
array, array,
@@ -13,6 +15,7 @@ import {
string, string,
} from "superstruct"; } from "superstruct";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-entity-picker";
import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace"; import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@@ -142,33 +145,33 @@ export class HuiConditionalCardEditor
<ha-entity-picker <ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
.value=${cond.entity} .value=${cond.entity}
.index=${idx} .idx=${idx}
.configValue=${"entity"} .configValue=${"entity"}
@change=${this._changeCondition} @change=${this._changeCondition}
allow-custom-entity allow-custom-entity
></ha-entity-picker> ></ha-entity-picker>
</div> </div>
<div class="state"> <div class="state">
<paper-dropdown-menu> <mwc-select
<paper-listbox .value=${cond.state_not !== undefined
.selected=${cond.state_not !== undefined ? 1 : 0} ? "true"
slot="dropdown-content" : "false"}
.index=${idx} .idx=${idx}
.configValue=${"invert"} .configValue=${"invert"}
@selected-item-changed=${this._changeCondition} @selected=${this._changeCondition}
> @closed=${stopPropagation}
<paper-item >
>${this.hass!.localize( <mwc-list-item value="false">
"ui.panel.lovelace.editor.card.conditional.state_equal" ${this.hass!.localize(
)}</paper-item "ui.panel.lovelace.editor.card.conditional.state_equal"
> )}
<paper-item </mwc-list-item>
>${this.hass!.localize( <mwc-list-item value="true">
"ui.panel.lovelace.editor.card.conditional.state_not_equal" ${this.hass!.localize(
)}</paper-item "ui.panel.lovelace.editor.card.conditional.state_not_equal"
> )}
</paper-listbox> </mwc-list-item>
</paper-dropdown-menu> </mwc-select>
<paper-input <paper-input
.label="${this.hass!.localize( .label="${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.state" "ui.panel.lovelace.editor.card.generic.state"
@@ -178,7 +181,7 @@ export class HuiConditionalCardEditor
.value=${cond.state_not !== undefined .value=${cond.state_not !== undefined
? cond.state_not ? cond.state_not
: cond.state} : cond.state}
.index=${idx} .idx=${idx}
.configValue=${"state"} .configValue=${"state"}
@value-changed=${this._changeCondition} @value-changed=${this._changeCondition}
></paper-input> ></paper-input>
@@ -274,9 +277,9 @@ export class HuiConditionalCardEditor
} }
const conditions = [...this._config.conditions]; const conditions = [...this._config.conditions];
if (target.configValue === "entity" && target.value === "") { if (target.configValue === "entity" && target.value === "") {
conditions.splice(target.index, 1); conditions.splice(target.idx, 1);
} else { } else {
const condition = { ...conditions[target.index] }; const condition = { ...conditions[target.idx] };
if (target.configValue === "entity") { if (target.configValue === "entity") {
condition.entity = target.value; condition.entity = target.value;
} else if (target.configValue === "state") { } else if (target.configValue === "state") {
@@ -286,7 +289,7 @@ export class HuiConditionalCardEditor
condition.state = target.value; condition.state = target.value;
} }
} else if (target.configValue === "invert") { } else if (target.configValue === "invert") {
if (target.selected === 1) { if (target.value === "true") {
if (condition.state) { if (condition.state) {
condition.state_not = condition.state; condition.state_not = condition.state;
delete condition.state; delete condition.state;
@@ -296,7 +299,7 @@ export class HuiConditionalCardEditor
delete condition.state_not; delete condition.state_not;
} }
} }
conditions[target.index] = condition; conditions[target.idx] = condition;
} }
this._config = { ...this._config, conditions }; this._config = { ...this._config, conditions };
fireEvent(this, "config-changed", { config: this._config }); fireEvent(this, "config-changed", { config: this._config });
@@ -321,7 +324,7 @@ export class HuiConditionalCardEditor
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
} }
.condition .state paper-dropdown-menu { .condition .state mwc-select {
margin-right: 16px; margin-right: 16px;
} }
.condition .state paper-input { .condition .state paper-input {

View File

@@ -1,6 +1,4 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { import {

View File

@@ -1,3 +1,5 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -99,37 +101,34 @@ export class HuiGenericEntityRowEditor
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-icon-picker> ></ha-icon-picker>
</div> </div>
<paper-dropdown-menu .label=${"Secondary Info"}> <mwc-select
<paper-listbox label="Secondary Info"
slot="dropdown-content" .selected=${this._config.secondary_info || "none"}
attr-for-selected="value" .configValue=${"secondary_info"}
.selected=${this._config.secondary_info || "none"} @selected=${this._valueChanged}
.configValue=${"secondary_info"} >
@iron-select=${this._valueChanged} <mwc-list-item value=""
>${this.hass!.localize(
"ui.panel.lovelace.editor.card.entities.secondary_info_values.none"
)}</mwc-list-item
> >
<paper-item value="" ${Object.keys(SecondaryInfoValues).map((info) => {
>${this.hass!.localize( if (
"ui.panel.lovelace.editor.card.entities.secondary_info_values.none" !("domains" in SecondaryInfoValues[info]) ||
)}</paper-item ("domains" in SecondaryInfoValues[info] &&
> SecondaryInfoValues[info].domains!.includes(domain))
${Object.keys(SecondaryInfoValues).map((info) => { ) {
if ( return html`
!("domains" in SecondaryInfoValues[info]) || <mwc-list-item .value=${info}>
("domains" in SecondaryInfoValues[info] && ${this.hass!.localize(
SecondaryInfoValues[info].domains!.includes(domain)) `ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}`
) { )}
return html` </mwc-list-item>
<paper-item .value=${info} `;
>${this.hass!.localize( }
`ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}` return "";
)}</paper-item })}
> </mwc-select>
`;
}
return "";
})}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
`; `;
} }

View File

@@ -1,11 +1,11 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { assert, boolean, object, optional, string, assign } from "superstruct"; import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/ha-switch"; import "../../../../components/ha-switch";
@@ -17,9 +17,9 @@ import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor"; import "../../components/hui-theme-select-editor";
import { LovelaceCardEditor } from "../../types"; import { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget } from "../types"; import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@@ -155,22 +155,24 @@ export class HuiPictureEntityCardEditor
allow-custom-entity allow-custom-entity
></ha-entity-picker> ></ha-entity-picker>
<div class="side-by-side"> <div class="side-by-side">
<paper-dropdown-menu <mwc-select
.label="${this.hass.localize( .label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.camera_view" "ui.panel.lovelace.editor.card.generic.camera_view"
)} (${this.hass.localize( )} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional" "ui.panel.lovelace.editor.card.config.optional"
)})" )})"
.configValue=${"camera_view"} .configValue=${"camera_view"}
@value-changed=${this._valueChanged} @selected=${this._valueChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${views.indexOf(this._camera_view)}
> >
<paper-listbox ${views.map(
slot="dropdown-content" (view) =>
.selected=${views.indexOf(this._camera_view)} html`<mwc-list-item .value=${view}>${view}</mwc-list-item> `
> )}
${views.map((view) => html` <paper-item>${view}</paper-item> `)} </mwc-select>
</paper-listbox>
</paper-dropdown-menu>
<paper-input <paper-input
.label="${this.hass.localize( .label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.aspect_ratio" "ui.panel.lovelace.editor.card.generic.aspect_ratio"

View File

@@ -1,11 +1,11 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { array, assert, object, optional, string, assign } from "superstruct"; import { array, assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-entity-picker";
import { ActionConfig } from "../../../../data/lovelace"; import { ActionConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@@ -17,10 +17,10 @@ import { EntityConfig } from "../../entity-rows/types";
import { LovelaceCardEditor } from "../../types"; import { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities"; import { processEditorEntities } from "../process-editor-entities";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entitiesConfigStruct } from "../structs/entities-struct"; import { entitiesConfigStruct } from "../structs/entities-struct";
import { EditorTarget } from "../types"; import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@@ -146,22 +146,24 @@ export class HuiPictureGlanceCardEditor
.includeDomains=${includeDomains} .includeDomains=${includeDomains}
></ha-entity-picker> ></ha-entity-picker>
<div class="side-by-side"> <div class="side-by-side">
<paper-dropdown-menu <mwc-select
.label="${this.hass.localize( .label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.camera_view" "ui.panel.lovelace.editor.card.generic.camera_view"
)} (${this.hass.localize( )} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional" "ui.panel.lovelace.editor.card.config.optional"
)})" )})"
.configValue=${"camera_view"} .configValue=${"camera_view"}
@value-changed=${this._valueChanged} @selected=${this._valueChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._camera_view}
> >
<paper-listbox ${views.map(
slot="dropdown-content" (view) =>
.selected=${views.indexOf(this._camera_view)} html`<mwc-list-item .value=${view}>${view}</mwc-list-item> `
> )}
${views.map((view) => html` <paper-item>${view}</paper-item> `)} </mwc-select>
</paper-listbox>
</paper-dropdown-menu>
<paper-input <paper-input
.label="${this.hass.localize( .label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.aspect_ratio" "ui.panel.lovelace.editor.card.generic.aspect_ratio"

View File

@@ -1,7 +1,6 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { import {
@@ -15,6 +14,7 @@ import {
union, union,
} from "superstruct"; } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeDomain } from "../../../../common/entity/compute_domain"; import { computeDomain } from "../../../../common/entity/compute_domain";
import { domainIcon } from "../../../../common/entity/domain_icon"; import { domainIcon } from "../../../../common/entity/domain_icon";
import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-entity-picker";
@@ -142,22 +142,24 @@ export class HuiSensorCardEditor
.configValue=${"icon"} .configValue=${"icon"}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-icon-picker> ></ha-icon-picker>
<paper-dropdown-menu <mwc-select
.label="${this.hass.localize( .label="${this.hass.localize(
"ui.panel.lovelace.editor.card.sensor.graph_type" "ui.panel.lovelace.editor.card.sensor.graph_type"
)} (${this.hass.localize( )} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional" "ui.panel.lovelace.editor.card.config.optional"
)})" )})"
.configValue=${"graph"} .configValue=${"graph"}
@value-changed=${this._valueChanged} @selected=${this._valueChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._graph}
> >
<paper-listbox ${graphs.map(
slot="dropdown-content" (graph) =>
.selected=${graphs.indexOf(this._graph)} html`<mwc-list-item .value=${graph}>${graph}</mwc-list-item>`
> )}
${graphs.map((graph) => html`<paper-item>${graph}</paper-item>`)} </mwc-select>
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
<div class="side-by-side"> <div class="side-by-side">
<paper-input <paper-input

View File

@@ -1,33 +1,35 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { import {
array, array,
assert, assert,
assign,
literal, literal,
number, number,
object, object,
optional, optional,
string, string,
union, union,
assign,
} from "superstruct"; } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-statistics-picker";
import "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import type { HaRadio } from "../../../../components/ha-radio";
import { StatisticType } from "../../../../data/history";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { StatisticsGraphCardConfig } from "../../cards/types"; import { StatisticsGraphCardConfig } from "../../cards/types";
import { processConfigEntities } from "../../common/process-config-entities";
import { LovelaceCardEditor } from "../../types"; import { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entitiesConfigStruct } from "../structs/entities-struct"; import { entitiesConfigStruct } from "../structs/entities-struct";
import { EditorTarget } from "../types"; import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import "../../../../components/entity/ha-statistics-picker";
import { processConfigEntities } from "../../common/process-config-entities";
import "../../../../components/ha-formfield";
import "../../../../components/ha-checkbox";
import { StatisticType } from "../../../../data/history";
import "../../../../components/ha-radio";
import type { HaRadio } from "../../../../components/ha-radio";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
const statTypeStruct = union([ const statTypeStruct = union([
literal("sum"), literal("sum"),
@@ -118,30 +120,28 @@ export class HuiStatisticsGraphCardEditor
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></paper-input> ></paper-input>
<div class="side-by-side"> <div class="side-by-side">
<paper-dropdown-menu <mwc-select
.label="${this.hass.localize( .label="${this.hass.localize(
"ui.panel.lovelace.editor.card.statistics-graph.period" "ui.panel.lovelace.editor.card.statistics-graph.period"
)} (${this.hass.localize( )} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional" "ui.panel.lovelace.editor.card.config.optional"
)})" )})"
.configValue=${"period"} .configValue=${"period"}
@iron-select=${this._periodSelected} @selected=${this._periodSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._period}
> >
<paper-listbox ${periods.map(
slot="dropdown-content" (period) =>
attr-for-selected="period" html`<mwc-list-item .value=${period}>
.selected=${this._period} ${this.hass!.localize(
> `ui.panel.lovelace.editor.card.statistics-graph.periods.${period}`
${periods.map( )}
(period) => </mwc-list-item>`
html`<paper-item .period=${period}> )}
${this.hass!.localize( </mwc-select>
`ui.panel.lovelace.editor.card.statistics-graph.periods.${period}`
)}
</paper-item>`
)}
</paper-listbox>
</paper-dropdown-menu>
<paper-input <paper-input
type="number" type="number"
.label="${this.hass.localize( .label="${this.hass.localize(
@@ -242,8 +242,8 @@ export class HuiStatisticsGraphCardEditor
}); });
} }
private _periodSelected(ev: CustomEvent) { private _periodSelected(ev) {
const newPeriod = ev.detail.item const newPeriod = ev.target.value
.period as StatisticsGraphCardConfig["period"]; .period as StatisticsGraphCardConfig["period"];
if (newPeriod === this._period) { if (newPeriod === this._period) {
return; return;

View File

@@ -1,5 +1,4 @@
import { mdiClose, mdiPencil, mdiPlus } from "@mdi/js"; import { mdiClose, mdiPencil, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";

View File

@@ -1,13 +1,13 @@
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-list/mwc-radio-list-item"; import "@material/mwc-list/mwc-radio-list-item";
import "@polymer/paper-item/paper-item"; import "@material/mwc-select/mwc-select";
import "@polymer/paper-listbox/paper-listbox";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { createCloseHeading } from "../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-icon"; import "../../../../components/ha-icon";
import "../../../../components/ha-paper-dropdown-menu";
import { import {
fetchConfig, fetchConfig,
fetchDashboards, fetchDashboards,
@@ -69,40 +69,37 @@ export class HuiDialogSelectView extends LitElement {
)} )}
> >
${this._params.allowDashboardChange ${this._params.allowDashboardChange
? html`<ha-paper-dropdown-menu ? html`<mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.lovelace.editor.select_view.dashboard_label" "ui.panel.lovelace.editor.select_view.dashboard_label"
)} )}
dynamic-align
.disabled=${!this._dashboards.length} .disabled=${!this._dashboards.length}
.value=${this._urlPath || this.hass.defaultPanel}
@selected=${this._dashboardChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
> >
<paper-listbox <mwc-list-item
slot="dropdown-content" value="lovelace"
.selected=${this._urlPath || this.hass.defaultPanel} .disabled=${(this.hass.panels.lovelace?.config as any)?.mode ===
@iron-select=${this._dashboardChanged} "yaml"}
attr-for-selected="url-path"
> >
<paper-item Default
.urlPath=${"lovelace"} </mwc-list-item>
.disabled=${(this.hass.panels.lovelace?.config as any) ${this._dashboards.map((dashboard) => {
?.mode === "yaml"} if (!this.hass.user!.is_admin && dashboard.require_admin) {
> return "";
Default }
</paper-item> return html`
${this._dashboards.map((dashboard) => { <mwc-list-item
if (!this.hass.user!.is_admin && dashboard.require_admin) { .disabled=${dashboard.mode !== "storage"}
return ""; .value=${dashboard.url_path}
} >${dashboard.title}</mwc-list-item
return html` >
<paper-item `;
.disabled=${dashboard.mode !== "storage"} })}
.urlPath=${dashboard.url_path} </mwc-select>`
>${dashboard.title}</paper-item
>
`;
})}
</paper-listbox>
</ha-paper-dropdown-menu>`
: ""} : ""}
${this._config ${this._config
? this._config.views.length > 1 ? this._config.views.length > 1
@@ -111,7 +108,7 @@ export class HuiDialogSelectView extends LitElement {
${this._config.views.map( ${this._config.views.map(
(view, idx) => html` (view, idx) => html`
<mwc-radio-list-item <mwc-radio-list-item
graphic=${this._config?.views.some(({ icon }) => icon) .graphic=${this._config?.views.some(({ icon }) => icon)
? "icon" ? "icon"
: null} : null}
@click=${this._viewChanged} @click=${this._viewChanged}
@@ -142,8 +139,8 @@ export class HuiDialogSelectView extends LitElement {
this._params!.dashboards || (await fetchDashboards(this.hass)); this._params!.dashboards || (await fetchDashboards(this.hass));
} }
private async _dashboardChanged(ev: CustomEvent) { private async _dashboardChanged(ev) {
let urlPath: string | null = ev.detail.item.urlPath; let urlPath: string | null = ev.target.value;
if (urlPath === this._urlPath) { if (urlPath === this._urlPath) {
return; return;
} }
@@ -181,7 +178,7 @@ export class HuiDialogSelectView extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-paper-dropdown-menu { mwc-select {
width: 100%; width: 100%;
} }
`, `,

View File

@@ -1,7 +1,10 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } 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 { stopPropagation } from "../../../../common/dom/stop_propagation";
import { slugify } from "../../../../common/string/slugify"; import { slugify } from "../../../../common/string/slugify";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker"; import "../../../../components/ha-icon-picker";
@@ -121,26 +124,24 @@ export class HuiViewEditor extends LitElement {
.configValue=${"theme"} .configValue=${"theme"}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></hui-theme-select-editor> ></hui-theme-select-editor>
<paper-dropdown-menu <mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.lovelace.editor.edit_view.type" "ui.panel.lovelace.editor.edit_view.type"
)} )}
.value=${this._type}
@selected=${this._typeChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
> >
<paper-listbox ${[DEFAULT_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT, PANEL_VIEW_LAYOUT].map(
slot="dropdown-content" (type) => html`<mwc-list-item .value=${type}>
.selected=${this._type} ${this.hass.localize(
attr-for-selected="type" `ui.panel.lovelace.editor.edit_view.types.${type}`
@iron-select=${this._typeChanged} )}
> </mwc-list-item>`
${[DEFAULT_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT, PANEL_VIEW_LAYOUT].map( )}
(type) => html`<paper-item .type=${type}> </mwc-select>
${this.hass.localize(
`ui.panel.lovelace.editor.edit_view.types.${type}`
)}
</paper-item>`
)}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
`; `;
} }
@@ -166,7 +167,7 @@ export class HuiViewEditor extends LitElement {
} }
private _typeChanged(ev): void { private _typeChanged(ev): void {
const selected = ev.target.selected; const selected = ev.target.value;
if (selected === "") { if (selected === "") {
return; return;
} }

View File

@@ -1,5 +1,5 @@
import "@polymer/paper-item/paper-icon-item"; import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-item/paper-item-body"; import "@material/mwc-select/mwc-select";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -68,19 +68,20 @@ export class HuiViewVisibilityEditor extends LitElement {
</p> </p>
${this._sortedUsers(this._users).map( ${this._sortedUsers(this._users).map(
(user) => html` (user) => html`
<paper-icon-item> <mwc-list-item graphic="avatar" hasMeta>
<ha-user-badge <ha-user-badge
slot="item-icon" slot="graphic"
.hass=${this.hass} .hass=${this.hass}
.user=${user} .user=${user}
></ha-user-badge> ></ha-user-badge>
<paper-item-body>${user.name}</paper-item-body> <span>${user.name}</span>
<ha-switch <ha-switch
slot="meta"
.userId=${user.id} .userId=${user.id}
@change=${this.valChange} @change=${this.valChange}
.checked=${this.checkUser(user.id)} .checked=${this.checkUser(user.id)}
></ha-switch> ></ha-switch>
</paper-icon-item> </mwc-list-item>
` `
)} )}
`; `;

View File

@@ -110,10 +110,11 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
const entityState = stateObj.state; const entityState = stateObj.state;
const controlButton = this._computeControlButton(stateObj); const controlButton = this._computeControlButton(stateObj);
const assumedState = stateObj.attributes.assumed_state === true;
const buttons = html` const buttons = html`
${!this._narrow && ${!this._narrow &&
entityState === "playing" && (entityState === "playing" || assumedState) &&
supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK) supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
? html` ? html`
<ha-icon-button <ha-icon-button
@@ -125,14 +126,15 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
></ha-icon-button> ></ha-icon-button>
` `
: ""} : ""}
${(entityState === "playing" && ${!assumedState &&
((entityState === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) || (supportsFeature(stateObj, SUPPORT_PAUSE) ||
supportsFeature(stateObj, SUPPORT_STOP))) || supportsFeature(stateObj, SUPPORT_STOP))) ||
((entityState === "paused" || entityState === "idle") && ((entityState === "paused" || entityState === "idle") &&
supportsFeature(stateObj, SUPPORT_PLAY)) || supportsFeature(stateObj, SUPPORT_PLAY)) ||
(entityState === "on" && (entityState === "on" &&
(supportsFeature(stateObj, SUPPORT_PLAY) || (supportsFeature(stateObj, SUPPORT_PLAY) ||
supportsFeature(stateObj, SUPPORT_PAUSE))) supportsFeature(stateObj, SUPPORT_PAUSE))))
? html` ? html`
<ha-icon-button <ha-icon-button
.path=${controlButton.icon} .path=${controlButton.icon}
@@ -143,7 +145,34 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
></ha-icon-button> ></ha-icon-button>
` `
: ""} : ""}
${entityState === "playing" && ${assumedState && supportsFeature(stateObj, SUPPORT_PLAY)
? html`
<ha-icon-button
.path=${mdiPlay}
.label=${this.hass.localize(`ui.card.media_player.media_play`)}
@click=${this._play}
></ha-icon-button>
`
: ""}
${assumedState && supportsFeature(stateObj, SUPPORT_PAUSE)
? html`
<ha-icon-button
.path=${mdiPause}
.label=${this.hass.localize(`ui.card.media_player.media_pause`)}
@click=${this._pause}
></ha-icon-button>
`
: ""}
${assumedState && supportsFeature(stateObj, SUPPORT_STOP)
? html`
<ha-icon-button
.path=${mdiStop}
.label=${this.hass.localize(`ui.card.media_player.media_stop`)}
@click=${this._stop}
></ha-icon-button>
`
: ""}
${(entityState === "playing" || assumedState) &&
supportsFeature(stateObj, SUPPORT_NEXT_TRACK) supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
? html` ? html`
<ha-icon-button <ha-icon-button
@@ -312,6 +341,24 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
}); });
} }
private _play(): void {
this.hass!.callService("media_player", "media_play", {
entity_id: this._config!.entity,
});
}
private _pause(): void {
this.hass!.callService("media_player", "media_pause", {
entity_id: this._config!.entity,
});
}
private _stop(): void {
this.hass!.callService("media_player", "media_stop", {
entity_id: this._config!.entity,
});
}
private _previousTrack(): void { private _previousTrack(): void {
this.hass!.callService("media_player", "media_previous_track", { this.hass!.callService("media_player", "media_previous_track", {
entity_id: this._config!.entity, entity_id: this._config!.entity,

View File

@@ -276,137 +276,153 @@ class HUIRoot extends LitElement {
></ha-icon-button> ></ha-icon-button>
` `
: ""} : ""}
<ha-button-menu corner="BOTTOM_START"> ${this._showButtonMenu
<ha-icon-button ? html`
slot="trigger" <ha-button-menu corner="BOTTOM_START">
.label=${this.hass!.localize( <ha-icon-button
"ui.panel.lovelace.editor.menu.open" slot="trigger"
)}
.path=${mdiDotsVertical}
></ha-icon-button>
${this.narrow &&
this._conversation(this.hass.config.components)
? html`
<mwc-list-item
.label=${this.hass!.localize( .label=${this.hass!.localize(
"ui.panel.lovelace.menu.start_conversation" "ui.panel.lovelace.editor.menu.open"
)} )}
graphic="icon" .path=${mdiDotsVertical}
@request-selected=${this._showVoiceCommandDialog} ></ha-icon-button>
> ${this.narrow &&
<span this._conversation(this.hass.config.components)
>${this.hass!.localize( ? html`
"ui.panel.lovelace.menu.start_conversation" <mwc-list-item
)}</span .label=${this.hass!.localize(
> "ui.panel.lovelace.menu.start_conversation"
<ha-svg-icon )}
slot="graphic" graphic="icon"
.path=${mdiMicrophone} @request-selected=${this
></ha-svg-icon> ._showVoiceCommandDialog}
</mwc-list-item> >
` <span
: ""} >${this.hass!.localize(
${this._yamlMode "ui.panel.lovelace.menu.start_conversation"
? html` )}</span
<mwc-list-item >
aria-label=${this.hass!.localize( <ha-svg-icon
"ui.common.refresh" slot="graphic"
)} .path=${mdiMicrophone}
graphic="icon" ></ha-svg-icon>
@request-selected=${this._handleRefresh} </mwc-list-item>
> `
<span : ""}
>${this.hass!.localize("ui.common.refresh")}</span ${this._yamlMode
> ? html`
<ha-svg-icon <mwc-list-item
slot="graphic" aria-label=${this.hass!.localize(
.path=${mdiRefresh} "ui.common.refresh"
></ha-svg-icon> )}
</mwc-list-item> graphic="icon"
<mwc-list-item @request-selected=${this._handleRefresh}
aria-label=${this.hass!.localize( >
"ui.panel.lovelace.unused_entities.title" <span
)} >${this.hass!.localize(
graphic="icon" "ui.common.refresh"
@request-selected=${this._handleUnusedEntities} )}</span
> >
<span <ha-svg-icon
>${this.hass!.localize( slot="graphic"
"ui.panel.lovelace.unused_entities.title" .path=${mdiRefresh}
)}</span ></ha-svg-icon>
> </mwc-list-item>
<ha-svg-icon <mwc-list-item
slot="graphic" aria-label=${this.hass!.localize(
.path=${mdiShape} "ui.panel.lovelace.unused_entities.title"
></ha-svg-icon> )}
</mwc-list-item> graphic="icon"
` @request-selected=${this
: ""} ._handleUnusedEntities}
${(this.hass.panels.lovelace?.config as LovelacePanelConfig) >
?.mode === "yaml" <span
? html` >${this.hass!.localize(
<mwc-list-item "ui.panel.lovelace.unused_entities.title"
graphic="icon" )}</span
aria-label=${this.hass!.localize( >
"ui.panel.lovelace.menu.reload_resources" <ha-svg-icon
)} slot="graphic"
@request-selected=${this._handleReloadResources} .path=${mdiShape}
> ></ha-svg-icon>
${this.hass!.localize( </mwc-list-item>
"ui.panel.lovelace.menu.reload_resources" `
)} : ""}
<ha-svg-icon ${(
slot="graphic" this.hass.panels.lovelace
.path=${mdiRefresh} ?.config as LovelacePanelConfig
></ha-svg-icon> )?.mode === "yaml"
</mwc-list-item> ? html`
` <mwc-list-item
: ""} graphic="icon"
${this.hass!.user?.is_admin && !this.hass!.config.safe_mode aria-label=${this.hass!.localize(
? html` "ui.panel.lovelace.menu.reload_resources"
<mwc-list-item )}
graphic="icon" @request-selected=${this
aria-label=${this.hass!.localize( ._handleReloadResources}
"ui.panel.lovelace.menu.configure_ui" >
)} ${this.hass!.localize(
@request-selected=${this._handleEnableEditMode} "ui.panel.lovelace.menu.reload_resources"
> )}
${this.hass!.localize( <ha-svg-icon
"ui.panel.lovelace.menu.configure_ui" slot="graphic"
)} .path=${mdiRefresh}
<ha-svg-icon ></ha-svg-icon>
slot="graphic" </mwc-list-item>
.path=${mdiPencil} `
></ha-svg-icon> : ""}
</mwc-list-item> ${this.hass!.user?.is_admin &&
` !this.hass!.config.safe_mode
: ""} ? html`
${this._editMode <mwc-list-item
? html` graphic="icon"
<a aria-label=${this.hass!.localize(
href=${documentationUrl(this.hass, "/lovelace/")} "ui.panel.lovelace.menu.configure_ui"
rel="noreferrer" )}
class="menu-link" @request-selected=${this
target="_blank" ._handleEnableEditMode}
> >
<mwc-list-item ${this.hass!.localize(
graphic="icon" "ui.panel.lovelace.menu.configure_ui"
aria-label=${this.hass!.localize( )}
"ui.panel.lovelace.menu.help" <ha-svg-icon
)} slot="graphic"
> .path=${mdiPencil}
${this.hass!.localize( ></ha-svg-icon>
"ui.panel.lovelace.menu.help" </mwc-list-item>
)} `
<ha-svg-icon : ""}
slot="graphic" ${this._editMode
.path=${mdiHelp} ? html`
></ha-svg-icon> <a
</mwc-list-item> href=${documentationUrl(
</a> this.hass,
` "/lovelace/"
: ""} )}
</ha-button-menu> rel="noreferrer"
class="menu-link"
target="_blank"
>
<mwc-list-item
graphic="icon"
aria-label=${this.hass!.localize(
"ui.panel.lovelace.menu.help"
)}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.help"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiHelp}
></ha-svg-icon>
</mwc-list-item>
</a>
`
: ""}
</ha-button-menu>
`
: ""}
</app-toolbar> </app-toolbar>
`} `}
${this._editMode ${this._editMode
@@ -621,6 +637,17 @@ class HUIRoot extends LitElement {
return this.shadowRoot!.getElementById("view") as HTMLDivElement; return this.shadowRoot!.getElementById("view") as HTMLDivElement;
} }
private get _showButtonMenu(): boolean {
return (
(this.narrow && this._conversation(this.hass.config.components)) ||
this._editMode ||
(this.hass!.user?.is_admin && !this.hass!.config.safe_mode) ||
(this.hass.panels.lovelace?.config as LovelacePanelConfig)?.mode ===
"yaml" ||
this._yamlMode
);
}
private _handleRefresh(ev: CustomEvent<RequestSelectedDetail>): void { private _handleRefresh(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) { if (!shouldHandleRequestSelectedEvent(ev)) {
return; return;

View File

@@ -25,7 +25,6 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { domainIcon } from "../../common/entity/domain_icon"; import { domainIcon } from "../../common/entity/domain_icon";
import { supportsFeature } from "../../common/entity/supports-feature"; import { supportsFeature } from "../../common/entity/supports-feature";
import { navigate } from "../../common/navigate";
import "../../components/ha-button-menu"; import "../../components/ha-button-menu";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import { UNAVAILABLE_STATES } from "../../data/entity"; import { UNAVAILABLE_STATES } from "../../data/entity";
@@ -47,6 +46,12 @@ import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-marquee"; import "../lovelace/components/hui-marquee";
import { BrowserMediaPlayer } from "./browser-media-player"; import { BrowserMediaPlayer } from "./browser-media-player";
declare global {
interface HASSDomEvents {
"player-picked": { entityId: string };
}
}
@customElement("ha-bar-media-player") @customElement("ha-bar-media-player")
class BarMediaPlayer extends LitElement { class BarMediaPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -399,7 +404,7 @@ class BarMediaPlayer extends LitElement {
private _selectPlayer(ev: CustomEvent): void { private _selectPlayer(ev: CustomEvent): void {
const entityId = (ev.currentTarget as any).player; const entityId = (ev.currentTarget as any).player;
navigate(`/media-browser/${entityId}`, { replace: true }); fireEvent(this, "player-picked", { entityId });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -15,6 +15,7 @@ import { LocalStorage } from "../../common/decorators/local-storage";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import "../../components/ha-menu-button"; import "../../components/ha-menu-button";
import "../../components/ha-circular-progress";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import "../../components/ha-svg-icon"; import "../../components/ha-svg-icon";
import "../../components/media-player/ha-media-player-browse"; import "../../components/media-player/ha-media-player-browse";
@@ -43,6 +44,16 @@ import {
isCameraMediaSource, isCameraMediaSource,
} from "../../data/camera"; } from "../../data/camera";
const createMediaPanelUrl = (entityId: string, items: MediaPlayerItemId[]) => {
let path = `/media-browser/${entityId}`;
for (const item of items.slice(1)) {
path +=
"/" +
encodeURIComponent(`${item.media_content_type},${item.media_content_id}`);
}
return path;
};
@customElement("ha-panel-media-browser") @customElement("ha-panel-media-browser")
class PanelMediaBrowser extends LitElement { class PanelMediaBrowser extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -54,6 +65,8 @@ class PanelMediaBrowser extends LitElement {
@state() _currentItem?: MediaPlayerItem; @state() _currentItem?: MediaPlayerItem;
@state() _uploading = 0;
private _navigateIds: MediaPlayerItemId[] = [ private _navigateIds: MediaPlayerItemId[] = [
{ {
media_content_id: undefined, media_content_id: undefined,
@@ -97,12 +110,34 @@ class PanelMediaBrowser extends LitElement {
) )
? html` ? html`
<mwc-button <mwc-button
.label=${this.hass.localize( .label=${this._uploading > 0
"ui.components.media-browser.file_management.add_media" ? this.hass.localize(
)} "ui.components.media-browser.file_management.uploading",
{
count: this._uploading,
}
)
: this.hass.localize(
"ui.components.media-browser.file_management.add_media"
)}
.disabled=${this._uploading > 0}
@click=${this._startUpload} @click=${this._startUpload}
> >
<ha-svg-icon .path=${mdiUpload} slot="icon"></ha-svg-icon> ${this._uploading > 0
? html`
<ha-circular-progress
size="tiny"
active
alt=""
slot="icon"
></ha-circular-progress>
`
: html`
<ha-svg-icon
.path=${mdiUpload}
slot="icon"
></ha-svg-icon>
`}
</mwc-button> </mwc-button>
` `
: ""} : ""}
@@ -120,6 +155,7 @@ class PanelMediaBrowser extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.entityId=${this._entityId} .entityId=${this._entityId}
.narrow=${this.narrow} .narrow=${this.narrow}
@player-picked=${this._playerPicked}
></ha-bar-media-player> ></ha-bar-media-player>
`; `;
} }
@@ -179,7 +215,9 @@ class PanelMediaBrowser extends LitElement {
} }
private _goBack() { private _goBack() {
history.back(); navigate(
createMediaPanelUrl(this._entityId, this._navigateIds.slice(0, -1))
);
} }
private _mediaBrowsed(ev: { detail: HASSDomEvents["media-browsed"] }) { private _mediaBrowsed(ev: { detail: HASSDomEvents["media-browsed"] }) {
@@ -188,15 +226,9 @@ class PanelMediaBrowser extends LitElement {
return; return;
} }
let path = ""; navigate(createMediaPanelUrl(this._entityId, ev.detail.ids), {
for (const item of ev.detail.ids.slice(1)) { replace: ev.detail.replace,
path += });
"/" +
encodeURIComponent(
`${item.media_content_type},${item.media_content_id}`
);
}
navigate(`/media-browser/${this._entityId}${path}`);
} }
private async _mediaPicked( private async _mediaPicked(
@@ -219,18 +251,18 @@ class PanelMediaBrowser extends LitElement {
return; return;
} }
if (item.media_content_type.startsWith("audio/")) { const resolvedUrl = await resolveMediaSource(
this.hass,
item.media_content_id
);
if (resolvedUrl.mime_type.startsWith("audio/")) {
await this.shadowRoot!.querySelector("ha-bar-media-player")!.playItem( await this.shadowRoot!.querySelector("ha-bar-media-player")!.playItem(
item item
); );
return; return;
} }
const resolvedUrl: any = await resolveMediaSource(
this.hass,
item.media_content_id
);
showWebBrowserPlayMediaDialog(this, { showWebBrowserPlayMediaDialog(this, {
sourceUrl: resolvedUrl.url, sourceUrl: resolvedUrl.url,
sourceType: resolvedUrl.mime_type, sourceType: resolvedUrl.mime_type,
@@ -239,29 +271,50 @@ class PanelMediaBrowser extends LitElement {
}); });
} }
private _playerPicked(ev) {
const entityId: string = ev.detail.entityId;
if (entityId === this._entityId) {
return;
}
navigate(createMediaPanelUrl(entityId, this._navigateIds));
}
private async _startUpload() { private async _startUpload() {
if (this._uploading > 0) {
return;
}
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.addEventListener("change", async () => { input.accept = "audio/*,video/*,image/*";
try { input.multiple = true;
await uploadLocalMedia( input.addEventListener(
this.hass, "change",
this._currentItem!.media_content_id!, async () => {
input.files![0] const files = input.files!;
); const target = this._currentItem!.media_content_id!;
} catch (err: any) {
showAlertDialog(this, { for (let i = 0; i < files.length; i++) {
text: this.hass.localize( this._uploading = files.length - i;
"ui.components.media-browser.file_management.upload_failed", try {
{ // eslint-disable-next-line no-await-in-loop
reason: err.message || err, await uploadLocalMedia(this.hass, target, files[i]);
} } catch (err: any) {
), showAlertDialog(this, {
}); text: this.hass.localize(
return; "ui.components.media-browser.file_management.upload_failed",
} {
await this._browser.refresh(); reason: err.message || err,
}); }
),
});
break;
}
}
this._uploading = 0;
await this._browser.refresh();
},
{ once: true }
);
input.click(); input.click();
} }
@@ -269,8 +322,10 @@ class PanelMediaBrowser extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
:host { app-toolbar mwc-button {
--mdc-theme-primary: var(--app-header-text-color); --mdc-theme-primary: var(--app-header-text-color);
/* We use icon + text to show disabled state */
--mdc-button-disabled-ink-color: var(--app-header-text-color);
} }
ha-media-player-browse { ha-media-player-browse {
@@ -288,7 +343,8 @@ class PanelMediaBrowser extends LitElement {
right: 0; right: 0;
} }
ha-svg-icon[slot="icon"] { ha-svg-icon[slot="icon"],
ha-circular-progress[slot="icon"] {
vertical-align: middle; vertical-align: middle;
} }
`, `,

View File

@@ -1,8 +1,6 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";

View File

@@ -1,12 +1,11 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../components/ha-paper-dropdown-menu";
import "../../components/ha-settings-row"; import "../../components/ha-settings-row";
import { fetchDashboards, LovelaceDashboard } from "../../data/lovelace"; import { fetchDashboards, LovelaceDashboard } from "../../data/lovelace";
import { setDefaultPanel } from "../../data/panel"; import { setDefaultPanel } from "../../data/panel";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
@customElement("ha-pick-dashboard-row") @customElement("ha-pick-dashboard-row")
class HaPickDashboardRow extends LitElement { class HaPickDashboardRow extends LitElement {
@@ -30,36 +29,30 @@ class HaPickDashboardRow extends LitElement {
<span slot="description"> <span slot="description">
${this.hass.localize("ui.panel.profile.dashboard.description")} ${this.hass.localize("ui.panel.profile.dashboard.description")}
</span> </span>
<ha-paper-dropdown-menu <mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.profile.dashboard.dropdown_label" "ui.panel.profile.dashboard.dropdown_label"
)} )}
dynamic-align
.disabled=${!this._dashboards.length} .disabled=${!this._dashboards.length}
.value=${this.hass.defaultPanel}
@selected=${this._dashboardChanged}
> >
<paper-listbox <mwc-list-item value="lovelace">
slot="dropdown-content" ${this.hass.localize(
.selected=${this.hass.defaultPanel} "ui.panel.profile.dashboard.default_dashboard_label"
@iron-select=${this._dashboardChanged} )}
attr-for-selected="url-path" </mwc-list-item>
> ${this._dashboards.map((dashboard) => {
<paper-item url-path="lovelace" if (!this.hass.user!.is_admin && dashboard.require_admin) {
>${this.hass.localize( return "";
"ui.panel.profile.dashboard.default_dashboard_label" }
)}</paper-item return html`
> <mwc-list-item .value=${dashboard.url_path}>
${this._dashboards.map((dashboard) => { ${dashboard.title}
if (!this.hass.user!.is_admin && dashboard.require_admin) { </mwc-list-item>
return ""; `;
} })}
return html` </mwc-select>
<paper-item url-path=${dashboard.url_path}
>${dashboard.title}</paper-item
>
`;
})}
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row> </ha-settings-row>
`; `;
} }
@@ -68,8 +61,8 @@ class HaPickDashboardRow extends LitElement {
this._dashboards = await fetchDashboards(this.hass); this._dashboards = await fetchDashboards(this.hass);
} }
private _dashboardChanged(ev: CustomEvent) { private _dashboardChanged(ev) {
const urlPath = ev.detail.item.getAttribute("url-path"); const urlPath = ev.target.value;
if (!urlPath || urlPath === this.hass.defaultPanel) { if (!urlPath || urlPath === this.hass.defaultPanel) {
return; return;
} }

View File

@@ -1,111 +0,0 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../components/ha-paper-dropdown-menu";
import "../../components/ha-settings-row";
import { EventsMixin } from "../../mixins/events-mixin";
import LocalizeMixin from "../../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
* @appliesMixin EventsMixin
*/
class HaPickLanguageRow extends LocalizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
a {
color: var(--primary-color);
}
paper-item {
direction: ltr;
}
paper-item[is-rtl] {
direction: rtl;
}
</style>
<ha-settings-row narrow="[[narrow]]">
<span slot="heading"
>[[localize('ui.panel.profile.language.header')]]</span
>
<span slot="description">
<a
href="https://developers.home-assistant.io/docs/en/internationalization_translation.html"
target="_blank"
rel="noreferrer"
>[[localize('ui.panel.profile.language.link_promo')]]</a
>
</span>
<ha-paper-dropdown-menu
label="[[localize('ui.panel.profile.language.dropdown_label')]]"
dynamic-align=""
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="language-tag"
selected="{{languageSelection}}"
>
<template is="dom-repeat" items="[[languages]]">
<paper-item language-tag$="[[item.key]]" is-rtl$="[[item.isRTL]]">
[[item.nativeName]]
</paper-item>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row>
`;
}
static get properties() {
return {
hass: Object,
narrow: Boolean,
languageSelection: {
type: String,
observer: "languageSelectionChanged",
},
languages: {
type: Array,
computed: "computeLanguages(hass)",
},
};
}
static get observers() {
return ["setLanguageSelection(language)"];
}
computeLanguages(hass) {
if (!hass || !hass.translationMetadata) {
return [];
}
const translations = hass.translationMetadata.translations;
return Object.keys(translations).map((key) => ({
key,
...translations[key],
}));
}
setLanguageSelection(language) {
this.languageSelection = language;
}
languageSelectionChanged(newVal) {
// Only fire event if language was changed. This prevents select updates when
// responding to hass changes.
if (newVal !== this.hass.language) {
this.fire("hass-language-select", newVal);
}
}
ready() {
super.ready();
if (this.hass && this.hass.locale && this.hass.locale.language) {
this.setLanguageSelection(this.hass.locale.language);
}
}
}
customElements.define("ha-pick-language-row", HaPickLanguageRow);

View File

@@ -0,0 +1,87 @@
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-settings-row";
import { HomeAssistant, Translation } from "../../types";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
@customElement("ha-pick-language-row")
export class HaPickLanguageRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
@state() private _languages: (Translation & { key: string })[] = [];
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._computeLanguages();
}
protected render() {
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading"
>${this.hass.localize("ui.panel.profile.language.header")}</span
>
<span slot="description">
<a
href="https://developers.home-assistant.io/docs/en/internationalization_translation.html"
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.profile.language.link_promo")}</a
>
</span>
<mwc-select
.label=${this.hass.localize(
"ui.panel.profile.language.dropdown_label"
)}
.value=${this.hass.locale.language}
@selected=${this._languageSelectionChanged}
>
${this._languages.map(
(language) => html`<mwc-list-item
.value=${language.key}
rtl=${language.isRTL}
>
${language.nativeName}
</mwc-list-item>`
)}
</mwc-select>
</ha-settings-row>
`;
}
private _computeLanguages() {
if (!this.hass.translationMetadata?.translations) {
return;
}
this._languages = Object.keys(
this.hass.translationMetadata.translations
).map((key) => ({
key,
...this.hass.translationMetadata.translations[key],
}));
}
private _languageSelectionChanged(ev) {
// Only fire event if language was changed. This prevents select updates when
// responding to hass changes.
if (ev.target.value !== this.hass.language) {
fireEvent(this, "hass-language-select", ev.target.value);
}
}
static styles = css`
a {
color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-pick-language-row": HaPickLanguageRow;
}
}

View File

@@ -1,11 +1,10 @@
import "@polymer/paper-item/paper-item"; import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-listbox/paper-listbox"; import "@material/mwc-select/mwc-select";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { formatNumber } from "../../common/number/format_number"; import { formatNumber } from "../../common/number/format_number";
import "../../components/ha-card"; import "../../components/ha-card";
import "../../components/ha-paper-dropdown-menu";
import "../../components/ha-settings-row"; import "../../components/ha-settings-row";
import { NumberFormat } from "../../data/translation"; import { NumberFormat } from "../../data/translation";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@@ -25,47 +24,39 @@ class NumberFormatRow extends LitElement {
<span slot="description"> <span slot="description">
${this.hass.localize("ui.panel.profile.number_format.description")} ${this.hass.localize("ui.panel.profile.number_format.description")}
</span> </span>
<ha-paper-dropdown-menu <mwc-select
label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.profile.number_format.dropdown_label" "ui.panel.profile.number_format.dropdown_label"
)} )}
dynamic-align
.disabled=${this.hass.locale === undefined} .disabled=${this.hass.locale === undefined}
.value=${this.hass.locale.number_format}
@selected=${this._handleFormatSelection}
> >
<paper-listbox ${Object.values(NumberFormat).map((format) => {
slot="dropdown-content" const formattedNumber = formatNumber(1234567.89, {
.selected=${this.hass.locale.number_format} ...this.hass.locale,
@iron-select=${this._handleFormatSelection} number_format: format,
attr-for-selected="format" });
> const value = this.hass.localize(
${Object.values(NumberFormat).map((format) => { `ui.panel.profile.number_format.formats.${format}`
const formattedNumber = formatNumber(1234567.89, { );
...this.hass.locale, const twoLine = value.slice(value.length - 2) !== "89"; // Display explicit number formats on one line
number_format: format, return html`
}); <mwc-list-item .value=${format} .twoline=${twoLine}>
const value = this.hass.localize( <span>${value}</span>
`ui.panel.profile.number_format.formats.${format}` ${twoLine
); ? html`<span slot="secondary">${formattedNumber}</span>`
const twoLine = value.slice(value.length - 2) !== "89"; // Display explicit number formats on one line : ""}
return html` </mwc-list-item>
<paper-item .format=${format} .label=${value}> `;
<paper-item-body ?two-line=${twoLine}> })}
<div>${value}</div> </mwc-select>
${twoLine
? html`<div secondary>${formattedNumber}</div>`
: ""}
</paper-item-body>
</paper-item>
`;
})}
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row> </ha-settings-row>
`; `;
} }
private async _handleFormatSelection(ev: CustomEvent) { private async _handleFormatSelection(ev) {
fireEvent(this, "hass-number-format-select", ev.detail.item.format); fireEvent(this, "hass-number-format-select", ev.target.value);
} }
} }

View File

@@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -13,7 +11,6 @@ import {
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 "../../components/ha-formfield"; import "../../components/ha-formfield";
import "../../components/ha-paper-dropdown-menu";
import "../../components/ha-radio"; import "../../components/ha-radio";
import type { HaRadio } from "../../components/ha-radio"; import type { HaRadio } from "../../components/ha-radio";
import "../../components/ha-settings-row"; import "../../components/ha-settings-row";
@@ -23,6 +20,8 @@ import {
} from "../../resources/ha-style"; } from "../../resources/ha-style";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import "@material/mwc-select/mwc-select";
import "@material/mwc-list/mwc-list-item";
@customElement("ha-pick-theme-row") @customElement("ha-pick-theme-row")
export class HaPickThemeRow extends LitElement { export class HaPickThemeRow extends LitElement {
@@ -63,22 +62,17 @@ export class HaPickThemeRow extends LitElement {
${this.hass.localize("ui.panel.profile.themes.link_promo")} ${this.hass.localize("ui.panel.profile.themes.link_promo")}
</a> </a>
</span> </span>
<ha-paper-dropdown-menu <mwc-select
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")} .label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
dynamic-align
.disabled=${!hasThemes} .disabled=${!hasThemes}
.value=${this.hass.selectedTheme?.theme || "Backend-selected"}
@selected=${this._handleThemeSelection}
> >
<paper-listbox ${this._themeNames.map(
slot="dropdown-content" (theme) =>
.selected=${this.hass.selectedTheme?.theme || "Backend-selected"} html`<mwc-list-item .value=${theme}>${theme}</mwc-list-item>`
attr-for-selected="theme" )}
@iron-select=${this._handleThemeSelection} </mwc-select>
>
${this._themeNames.map(
(theme) => html`<paper-item .theme=${theme}>${theme}</paper-item>`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row> </ha-settings-row>
${curTheme === "default" || this._supportsModeSelection(curTheme) ${curTheme === "default" || this._supportsModeSelection(curTheme)
? html` <div class="inputs"> ? html` <div class="inputs">
@@ -91,7 +85,7 @@ export class HaPickThemeRow extends LitElement {
@change=${this._handleDarkMode} @change=${this._handleDarkMode}
name="dark_mode" name="dark_mode"
value="auto" value="auto"
?checked=${themeSettings?.dark === undefined} .checked=${themeSettings?.dark === undefined}
></ha-radio> ></ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield <ha-formfield
@@ -103,7 +97,7 @@ export class HaPickThemeRow extends LitElement {
@change=${this._handleDarkMode} @change=${this._handleDarkMode}
name="dark_mode" name="dark_mode"
value="light" value="light"
?checked=${themeSettings?.dark === false} .checked=${themeSettings?.dark === false}
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
@@ -116,7 +110,7 @@ export class HaPickThemeRow extends LitElement {
@change=${this._handleDarkMode} @change=${this._handleDarkMode}
name="dark_mode" name="dark_mode"
value="dark" value="dark"
?checked=${themeSettings?.dark === true} .checked=${themeSettings?.dark === true}
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
@@ -195,8 +189,8 @@ export class HaPickThemeRow extends LitElement {
fireEvent(this, "settheme", { dark }); fireEvent(this, "settheme", { dark });
} }
private _handleThemeSelection(ev: CustomEvent) { private _handleThemeSelection(ev) {
const theme = ev.detail.item.theme; const theme = ev.target.value;
if (theme === "Backend-selected") { if (theme === "Backend-selected") {
if (this.hass.selectedTheme?.theme) { if (this.hass.selectedTheme?.theme) {
fireEvent(this, "settheme", { fireEvent(this, "settheme", {

View File

@@ -1,14 +1,13 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { formatTime } from "../../common/datetime/format_time"; import { formatTime } from "../../common/datetime/format_time";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-card"; import "../../components/ha-card";
import "../../components/ha-paper-dropdown-menu";
import "../../components/ha-settings-row"; import "../../components/ha-settings-row";
import { TimeFormat } from "../../data/translation"; import { TimeFormat } from "../../data/translation";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
@customElement("ha-pick-time-format-row") @customElement("ha-pick-time-format-row")
class TimeFormatRow extends LitElement { class TimeFormatRow extends LitElement {
@@ -26,42 +25,34 @@ class TimeFormatRow extends LitElement {
<span slot="description"> <span slot="description">
${this.hass.localize("ui.panel.profile.time_format.description")} ${this.hass.localize("ui.panel.profile.time_format.description")}
</span> </span>
<ha-paper-dropdown-menu <mwc-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.profile.time_format.dropdown_label" "ui.panel.profile.time_format.dropdown_label"
)} )}
dynamic-align
.disabled=${this.hass.locale === undefined} .disabled=${this.hass.locale === undefined}
.value=${this.hass.locale.time_format}
@selected=${this._handleFormatSelection}
> >
<paper-listbox ${Object.values(TimeFormat).map((format) => {
slot="dropdown-content" const formattedTime = formatTime(date, {
.selected=${this.hass.locale.time_format} ...this.hass.locale,
@iron-select=${this._handleFormatSelection} time_format: format,
attr-for-selected="format" });
> const value = this.hass.localize(
${Object.values(TimeFormat).map((format) => { `ui.panel.profile.time_format.formats.${format}`
const formattedTime = formatTime(date, { );
...this.hass.locale, return html`<mwc-list-item .value=${format} twoline>
time_format: format, <span>${value}</span>
}); <span slot="secondary">${formattedTime}</span>
const value = this.hass.localize( </mwc-list-item>`;
`ui.panel.profile.time_format.formats.${format}` })}
); </mwc-select>
return html` <paper-item .format=${format} .label=${value}>
<paper-item-body two-line>
<div>${value}</div>
<div secondary>${formattedTime}</div>
</paper-item-body>
</paper-item>`;
})}
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row> </ha-settings-row>
`; `;
} }
private async _handleFormatSelection(ev: CustomEvent) { private async _handleFormatSelection(ev) {
fireEvent(this, "hass-time-format-select", ev.detail.item.format); fireEvent(this, "hass-time-format-select", ev.target.value);
} }
} }

View File

@@ -525,8 +525,10 @@
"no_media_folder": "It looks like you have not yet created a media directory.", "no_media_folder": "It looks like you have not yet created a media directory.",
"setup_local_help": "Check the {documentation} on how to setup local media.", "setup_local_help": "Check the {documentation} on how to setup local media.",
"file_management": { "file_management": {
"highlight_button": "Click here to upload your first media",
"upload_failed": "Upload failed: {reason}", "upload_failed": "Upload failed: {reason}",
"add_media": "Add Media" "add_media": "Add Media",
"uploading": "Uploading {count} {count, plural,\n one {file}\n other {files}\n}"
}, },
"class": { "class": {
"album": "Album", "album": "Album",
@@ -2478,6 +2480,8 @@
"rename_dialog": "Edit the name of this config entry", "rename_dialog": "Edit the name of this config entry",
"rename_input_label": "Entry name", "rename_input_label": "Entry name",
"search": "Search integrations", "search": "Search integrations",
"add_zwave_js_device": "Add Z-Wave device",
"add_zha_device": "Add Zigbee device",
"disable": { "disable": {
"show_disabled": "Show disabled integrations", "show_disabled": "Show disabled integrations",
"disabled_integrations": "{number} disabled", "disabled_integrations": "{number} disabled",
@@ -3687,6 +3691,18 @@
"media-browser": { "media-browser": {
"error": { "error": {
"player_not_exist": "Media player {name} does not exist" "player_not_exist": "Media player {name} does not exist"
},
"tts": {
"message": "Message",
"example_message": "Hello {name}, you can play any text on any supported media player!",
"language": "Language",
"gender": "Gender",
"gender_male": "Male",
"gender_female": "Female",
"action_play": "Say",
"action_pick": "Select",
"set_as_default": "Set as default options",
"faild_to_store_defaults": "Failed to store defaults: {error}"
} }
}, },
"map": { "map": {

View File

@@ -10,6 +10,10 @@ describe("canToggleState", () => {
turn_off: null, turn_off: null,
}, },
}, },
states: {
"light.bla": { entity_id: "light.bla" },
"light.test": { entity_id: "light.test" },
},
}; };
it("Detects lights toggle", () => { it("Detects lights toggle", () => {
@@ -24,7 +28,11 @@ describe("canToggleState", () => {
const stateObj: any = { const stateObj: any = {
entity_id: "group.bla", entity_id: "group.bla",
state: "on", state: "on",
attributes: {
entity_id: ["light.bla", "light.test"],
},
}; };
assert.isTrue(canToggleState(hass, stateObj)); assert.isTrue(canToggleState(hass, stateObj));
}); });