mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-14 03:39:26 +00:00
Compare commits
3 Commits
20220301.1
...
fix-search
Author | SHA1 | Date | |
---|---|---|---|
![]() |
21e441b682 | ||
![]() |
737f7ba6b9 | ||
![]() |
a5862b86ca |
@@ -16,9 +16,6 @@
|
||||
"runem.lit-plugin",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
"containerEnv": {
|
||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||
},
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"files.eol": "\n",
|
||||
|
631
.yarn/releases/yarn-3.0.2.cjs
vendored
Executable file
631
.yarn/releases/yarn-3.0.2.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
785
.yarn/releases/yarn-3.2.0.cjs
vendored
785
.yarn/releases/yarn-3.2.0.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-3.0.2.cjs
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
This is the repository for the official [Home Assistant](https://home-assistant.io) frontend.
|
||||
|
||||
[](https://demo.home-assistant.io/)
|
||||
[](https://demo.home-assistant.io/)
|
||||
|
||||
- [View demo of Home Assistant](https://demo.home-assistant.io/)
|
||||
- [More information about Home Assistant](https://home-assistant.io)
|
||||
|
@@ -33,10 +33,6 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
|
||||
require.resolve(
|
||||
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);
|
||||
|
||||
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
|
@@ -3,7 +3,7 @@
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { marked } = require("marked");
|
||||
const marked = require("marked");
|
||||
const glob = require("glob");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
|
@@ -7,7 +7,7 @@ const source = require("vinyl-source-stream");
|
||||
const vinylBuffer = require("vinyl-buffer");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const flatmap = require("gulp-flatmap");
|
||||
const foreach = require("gulp-foreach");
|
||||
const merge = require("gulp-merge-json");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
@@ -183,7 +183,7 @@ gulp.task("build-merged-translations", () =>
|
||||
})
|
||||
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||
.pipe(
|
||||
flatmap((stream, file) => {
|
||||
foreach((stream, file) => {
|
||||
// For each language generate a merged json file. It begins with the master
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace";
|
||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||
@@ -20,8 +20,6 @@ class HcLovelace extends LitElement {
|
||||
|
||||
@property() public urlPath: string | null = null;
|
||||
|
||||
@query("hui-view") private _huiView?: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const index = this._viewIndex;
|
||||
if (index === undefined) {
|
||||
@@ -80,12 +78,12 @@ class HcLovelace extends LitElement {
|
||||
this.lovelaceConfig.background;
|
||||
|
||||
if (configBackground) {
|
||||
this._huiView!.style.setProperty(
|
||||
(this.shadowRoot!.querySelector(
|
||||
"hui-view"
|
||||
) as HTMLElement)!.style.setProperty(
|
||||
"--lovelace-background",
|
||||
configBackground
|
||||
);
|
||||
} else {
|
||||
this._huiView!.style.removeProperty("--lovelace-background");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,9 +116,6 @@ class HcLovelace extends LitElement {
|
||||
:host > * {
|
||||
flex: 1;
|
||||
}
|
||||
hui-view {
|
||||
background: var(--lovelace-background, var(--primary-background-color));
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -2,3 +2,8 @@ import "../../src/resources/ha-style";
|
||||
import "../../src/resources/roboto";
|
||||
import "../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./ha-demo";
|
||||
|
||||
/* polyfill for paper-dropdown */
|
||||
setTimeout(() => {
|
||||
import("web-animations-js/web-animations-next-lite.min");
|
||||
}, 1000);
|
||||
|
@@ -20,6 +20,7 @@ module.exports = [
|
||||
"editor-trigger",
|
||||
"editor-condition",
|
||||
"editor-action",
|
||||
"selectors",
|
||||
"trace",
|
||||
"trace-timeline",
|
||||
],
|
||||
|
@@ -3,20 +3,10 @@ import { html, css, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { describeAction } from "../../../../src/data/script_i18n";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("scene", "kitchen_morning", "scening", {
|
||||
friendly_name: "Kitchen Morning",
|
||||
}),
|
||||
getEntity("media_player", "kitchen", "playing", {
|
||||
friendly_name: "Sonos Kitchen",
|
||||
}),
|
||||
];
|
||||
|
||||
const ACTIONS = [
|
||||
const actions = [
|
||||
{ wait_template: "{{ true }}", alias: "Something with an alias" },
|
||||
{ delay: "0:05" },
|
||||
{ wait_template: "{{ true }}" },
|
||||
@@ -29,20 +19,8 @@ const ACTIONS = [
|
||||
device_id: "abcdefgh",
|
||||
domain: "plex",
|
||||
entity_id: "media_player.kitchen",
|
||||
type: "turn_on",
|
||||
},
|
||||
{ scene: "scene.kitchen_morning" },
|
||||
{
|
||||
service: "scene.turn_on",
|
||||
target: { entity_id: "scene.kitchen_morning" },
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
service: "media_player.play_media",
|
||||
target: { entity_id: "media_player.kitchen" },
|
||||
data: { media_content_id: "", media_content_type: "" },
|
||||
metadata: { title: "Happy Song" },
|
||||
},
|
||||
{
|
||||
wait_for_trigger: [
|
||||
{
|
||||
@@ -74,7 +52,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<ha-card header="Actions">
|
||||
${ACTIONS.map(
|
||||
${actions.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, conf as any)}</span>
|
||||
@@ -90,7 +68,6 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
super.firstUpdated(changedProps);
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
@@ -14,7 +14,7 @@ import { HaDelayAction } from "../../../../src/panels/config/automation/action/t
|
||||
import { HaDeviceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
|
||||
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
|
||||
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
|
||||
import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-activate_scene";
|
||||
import { HaSceneAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-scene";
|
||||
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
|
||||
import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
|
||||
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
|
||||
|
3
gallery/src/pages/automation/selectors.markdown
Normal file
3
gallery/src/pages/automation/selectors.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Selectors
|
||||
---
|
102
gallery/src/pages/automation/selectors.ts
Normal file
102
gallery/src/pages/automation/selectors.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { Selector } from "../../../../src/data/selector";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
|
||||
const SCHEMAS: { name: string; selector: Selector }[] = [
|
||||
{ name: "Addon", selector: { addon: {} } },
|
||||
|
||||
{ name: "Entity", selector: { entity: {} } },
|
||||
{ name: "Device", selector: { device: {} } },
|
||||
{ name: "Area", selector: { area: {} } },
|
||||
{ name: "Target", selector: { target: {} } },
|
||||
{
|
||||
name: "Number",
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "Boolean", selector: { boolean: {} } },
|
||||
{ name: "Time", selector: { time: {} } },
|
||||
{ name: "Action", selector: { action: {} } },
|
||||
{ name: "Text", selector: { text: { multiline: false } } },
|
||||
{ name: "Text Multiline", selector: { text: { multiline: true } } },
|
||||
{ name: "Object", selector: { object: {} } },
|
||||
{
|
||||
name: "Select",
|
||||
selector: {
|
||||
select: {
|
||||
options: ["Everyone Home", "Some Home", "All gone"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-selectors")
|
||||
class DemoHaSelector extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
private data: any = SCHEMAS.map(() => undefined);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const valueChanged = (ev) => {
|
||||
const sampleIdx = ev.target.sampleIdx;
|
||||
this.data[sampleIdx] = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
${SCHEMAS.map(
|
||||
(info, sampleIdx) => html`
|
||||
<demo-black-white-row
|
||||
.title=${info.name}
|
||||
.value=${{ selector: info.selector, data: this.data[sampleIdx] }}
|
||||
>
|
||||
${["light", "dark"].map(
|
||||
(slot) =>
|
||||
html`
|
||||
<ha-selector
|
||||
slot=${slot}
|
||||
.hass=${this.hass}
|
||||
.selector=${info.selector}
|
||||
.label=${info.name}
|
||||
.value=${this.data[sampleIdx]}
|
||||
.sampleIdx=${sampleIdx}
|
||||
@value-changed=${valueChanged}
|
||||
></ha-selector>
|
||||
`
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-selectors": DemoHaSelector;
|
||||
}
|
||||
}
|
@@ -36,9 +36,6 @@ const SCHEMAS: {
|
||||
text_multiline: "Text Multiline",
|
||||
object: "Object",
|
||||
select: "Select",
|
||||
icon: "Icon",
|
||||
media: "Media",
|
||||
location: "Location",
|
||||
},
|
||||
schema: [
|
||||
{ name: "addon", selector: { addon: {} } },
|
||||
@@ -64,22 +61,6 @@ const SCHEMAS: {
|
||||
select: { options: ["Everyone Home", "Some Home", "All gone"] },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "media",
|
||||
selector: {
|
||||
media: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -1,5 +1,3 @@
|
||||
---
|
||||
title: Selectors
|
||||
title: Target Selectors
|
||||
---
|
||||
|
||||
See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/).
|
||||
|
@@ -12,100 +12,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
friendly_name: "Alarm",
|
||||
}),
|
||||
getEntity("media_player", "livingroom", "playing", {
|
||||
friendly_name: "Livingroom",
|
||||
}),
|
||||
getEntity("media_player", "lounge", "idle", {
|
||||
friendly_name: "Lounge",
|
||||
supported_features: 444983,
|
||||
}),
|
||||
getEntity("light", "bedroom", "on", {
|
||||
friendly_name: "Bedroom",
|
||||
}),
|
||||
getEntity("switch", "coffee", "off", {
|
||||
friendly_name: "Coffee",
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES = [
|
||||
{
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_1",
|
||||
identifiers: [["demo", "volume1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Dishwasher",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_2",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: null,
|
||||
name: "Lamp",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
id: "device_3",
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
name_by_user: "User name",
|
||||
name: "Technical name",
|
||||
sw_version: null,
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
name: "Backyard",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
name: "Bedroom",
|
||||
picture: null,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
name: "Livingroom",
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
|
||||
const SCHEMAS: {
|
||||
name: string;
|
||||
@@ -166,20 +72,13 @@ const SCHEMAS: {
|
||||
name: "Select",
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
},
|
||||
icon: { name: "Icon", selector: { icon: {} } },
|
||||
media: { name: "Media", selector: { media: {} } },
|
||||
location: { name: "Location", selector: { location: {} } },
|
||||
location_radius: {
|
||||
name: "Location with radius",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-selector")
|
||||
class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
@state() public hass!: HomeAssistant;
|
||||
class DemoHaSelector extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
private data = SCHEMAS.map(() => ({}));
|
||||
|
||||
@@ -188,130 +87,12 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
hass.mockWS("auth/sign_path", (params) => params);
|
||||
hass.mockWS("media_player/browse_media", this._browseMedia);
|
||||
}
|
||||
|
||||
public provideHass(el) {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("show-dialog", this._dialogManager);
|
||||
}
|
||||
|
||||
private _browseMedia = ({ media_content_id }) => {
|
||||
if (media_content_id === undefined) {
|
||||
return {
|
||||
title: "Media",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/.",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: "directory",
|
||||
thumbnail: null,
|
||||
children: [
|
||||
{
|
||||
title: "Misc",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/misc",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: null,
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
title: "Movies",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/movies",
|
||||
can_play: true,
|
||||
can_expand: true,
|
||||
children_media_class: "movie",
|
||||
thumbnail: null,
|
||||
},
|
||||
{
|
||||
title: "Music",
|
||||
media_class: "album",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/music",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: "music",
|
||||
thumbnail: "/images/album_cover_2.jpg",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Subfolder",
|
||||
media_class: "directory",
|
||||
media_content_type: "",
|
||||
media_content_id: "media-source://media_source/local/sub",
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: "directory",
|
||||
thumbnail: null,
|
||||
children: [
|
||||
{
|
||||
title: "audio.mp3",
|
||||
media_class: "music",
|
||||
media_content_type: "audio/mpeg",
|
||||
media_content_id: "media-source://media_source/local/audio.mp3",
|
||||
can_play: true,
|
||||
can_expand: false,
|
||||
children_media_class: null,
|
||||
thumbnail: "/images/album_cover.jpg",
|
||||
},
|
||||
{
|
||||
title: "image.jpg",
|
||||
media_class: "image",
|
||||
media_content_type: "image/jpeg",
|
||||
media_content_id: "media-source://media_source/local/image.jpg",
|
||||
can_play: true,
|
||||
can_expand: false,
|
||||
children_media_class: null,
|
||||
thumbnail: "https://brands.home-assistant.io/_/image/logo.png",
|
||||
},
|
||||
{
|
||||
title: "movie.mp4",
|
||||
media_class: "movie",
|
||||
media_content_type: "image/jpeg",
|
||||
media_content_id: "media-source://media_source/local/movie.mp4",
|
||||
can_play: true,
|
||||
can_expand: false,
|
||||
children_media_class: null,
|
||||
thumbnail: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
private _dialogManager = (e) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
|
||||
showDialog(
|
||||
this,
|
||||
this.shadowRoot!,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${SCHEMAS.map((info, idx) => {
|
||||
@@ -350,6 +131,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
paper-input,
|
||||
ha-selector {
|
||||
width: 60;
|
||||
}
|
||||
|
@@ -29,7 +29,6 @@ const createConfigEntry = (
|
||||
source: "zeroconf",
|
||||
state: "loaded",
|
||||
supports_options: false,
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
disabled_by: null,
|
||||
pref_disable_new_entities: false,
|
||||
|
@@ -14,7 +14,7 @@ import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import "../../../src/components/search-input";
|
||||
import "../../../src/common/search/search-input";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
@@ -110,6 +110,8 @@ class HassioAddonStore extends LitElement {
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
no-label-float
|
||||
no-underline
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input>
|
||||
@@ -219,14 +221,13 @@ class HassioAddonStore extends LitElement {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding: 0 16px;
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
.search search-input {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.advanced {
|
||||
padding: 12px;
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -9,11 +11,10 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
|
||||
import "web-animations-js/web-animations-next-lite.min";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-select";
|
||||
import {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
@@ -56,44 +57,49 @@ class HassioAddonAudio extends LitElement {
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this._inputDevices &&
|
||||
html`<ha-select
|
||||
|
||||
<paper-dropdown-menu
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.input"
|
||||
)}
|
||||
@selected=${this._setInputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this._selectedInput!}
|
||||
@iron-select=${this._setInputDevice}
|
||||
>
|
||||
${this._inputDevices.map(
|
||||
(item) => html`
|
||||
<mwc-list-item .value=${item.device || ""}>
|
||||
${item.name}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>`}
|
||||
${this._outputDevices &&
|
||||
html`<ha-select
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="device"
|
||||
.selected=${this._selectedInput!}
|
||||
>
|
||||
${this._inputDevices &&
|
||||
this._inputDevices.map(
|
||||
(item) => html`
|
||||
<paper-item device=${item.device || ""}>
|
||||
${item.name}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-dropdown-menu
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.output"
|
||||
)}
|
||||
@selected=${this._setOutputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this._selectedOutput!}
|
||||
@iron-select=${this._setOutputDevice}
|
||||
>
|
||||
${this._outputDevices.map(
|
||||
(item) => html`
|
||||
<mwc-list-item .value=${item.device || ""}
|
||||
>${item.name}</mwc-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>`}
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="device"
|
||||
.selected=${this._selectedOutput!}
|
||||
>
|
||||
${this._outputDevices &&
|
||||
this._outputDevices.map(
|
||||
(item) => html`
|
||||
<paper-item device=${item.device || ""}
|
||||
>${item.name}</paper-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button @click=${this._saveSettings}>
|
||||
@@ -110,7 +116,8 @@ class HassioAddonAudio extends LitElement {
|
||||
hassioStyle,
|
||||
css`
|
||||
:host,
|
||||
ha-card {
|
||||
ha-card,
|
||||
paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
paper-item {
|
||||
@@ -119,30 +126,24 @@ class HassioAddonAudio extends LitElement {
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select:last-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
protected update(changedProperties: PropertyValues): void {
|
||||
super.update(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._addonChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _setInputDevice(ev): void {
|
||||
const device = ev.target.value;
|
||||
const device = ev.detail.item.getAttribute("device");
|
||||
this._selectedInput = device;
|
||||
}
|
||||
|
||||
private _setOutputDevice(ev): void {
|
||||
const device = ev.target.value;
|
||||
const device = ev.detail.item.getAttribute("device");
|
||||
this._selectedOutput = device;
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,6 @@ import {
|
||||
mdiFlask,
|
||||
mdiHomeAssistant,
|
||||
mdiKey,
|
||||
mdiLinkLock,
|
||||
mdiNetwork,
|
||||
mdiNumeric1,
|
||||
mdiNumeric2,
|
||||
@@ -17,8 +16,6 @@ import {
|
||||
mdiNumeric4,
|
||||
mdiNumeric5,
|
||||
mdiNumeric6,
|
||||
mdiNumeric7,
|
||||
mdiNumeric8,
|
||||
mdiPound,
|
||||
mdiShield,
|
||||
} from "@mdi/js";
|
||||
@@ -34,7 +31,6 @@ import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-chip";
|
||||
import "../../../../src/components/ha-chip-set";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
@@ -88,8 +84,6 @@ const RATING_ICON = {
|
||||
4: mdiNumeric4,
|
||||
5: mdiNumeric5,
|
||||
6: mdiNumeric6,
|
||||
7: mdiNumeric7,
|
||||
8: mdiNumeric8,
|
||||
};
|
||||
|
||||
@customElement("hassio-addon-info")
|
||||
@@ -215,7 +209,7 @@ class HassioAddonInfo extends LitElement {
|
||||
>`}
|
||||
</div>
|
||||
|
||||
<ha-chip-set class="capabilities">
|
||||
<div class="capabilities">
|
||||
${this.addon.stage !== "stable"
|
||||
? html` <ha-chip
|
||||
hasIcon
|
||||
@@ -240,9 +234,9 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-chip
|
||||
hasIcon
|
||||
class=${classMap({
|
||||
green: Number(this.addon.rating) >= 6,
|
||||
yellow: [3, 4, 5].includes(Number(this.addon.rating)),
|
||||
red: Number(this.addon.rating) >= 2,
|
||||
green: [5, 6].includes(Number(this.addon.rating)),
|
||||
yellow: [3, 4].includes(Number(this.addon.rating)),
|
||||
red: [1, 2].includes(Number(this.addon.rating)),
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="rating"
|
||||
@@ -370,17 +364,7 @@ class HassioAddonInfo extends LitElement {
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
${this.addon.signed
|
||||
? html`
|
||||
<ha-chip hasIcon @click=${this._showMoreInfo} id="signed">
|
||||
<ha-svg-icon slot="icon" .path=${mdiLinkLock}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.signed"
|
||||
)}
|
||||
</ha-chip>
|
||||
`
|
||||
: ""}
|
||||
</ha-chip-set>
|
||||
</div>
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { mdiFolder, mdiHomeAssistant, mdiPuzzle } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
@@ -92,8 +92,6 @@ export class SupervisorBackupContent extends LitElement {
|
||||
|
||||
@property() public confirmBackupPassword = "";
|
||||
|
||||
@query("paper-input, ha-radio, ha-checkbox", true) private _focusTarget;
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
@@ -111,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public override focus() {
|
||||
this._focusTarget?.focus();
|
||||
}
|
||||
|
||||
private _localize = (string: string) =>
|
||||
this.supervisor?.localize(`backup.${string}`) ||
|
||||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
|
||||
@@ -175,23 +169,24 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ""}
|
||||
${this.backupType === "partial"
|
||||
? html`<div class="partial-picker">
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup
|
||||
? this.backup.homeassistant
|
||||
: this.hass.config.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@click=${this.toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
|
||||
${this.backup && this.backup.homeassistant
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup.homeassistant}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@click=${this.toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
${foldersSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
|
@@ -148,6 +148,7 @@ export class HassioUpdate extends LitElement {
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
--paper-item-body-two-line-min-height: 32px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -64,7 +64,6 @@ export class DialogHassioBackupUpload
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
dialogInitialFocus
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
|
@@ -92,7 +92,6 @@ class HassioBackupDialog
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
.localize=${this._dialogParams.localize}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>`}
|
||||
${this._error
|
||||
|
@@ -61,7 +61,6 @@ class HassioCreateBackupDialog extends LitElement {
|
||||
: html`<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>`}
|
||||
${this._error
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
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 { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/components/ha-select";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
@@ -89,20 +90,18 @@ class HassioDatadiskDialog extends LitElement {
|
||||
)}
|
||||
<br /><br />
|
||||
|
||||
<ha-select
|
||||
<paper-dropdown-menu
|
||||
.label=${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.select_device"
|
||||
)}
|
||||
@selected=${this._select_device}
|
||||
dialogInitialFocus
|
||||
@value-changed=${this._select_device}
|
||||
>
|
||||
${this.devices.map(
|
||||
(device) =>
|
||||
html`<mwc-list-item .value=${device}
|
||||
>${device}</mwc-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
<paper-listbox slot="dropdown-content">
|
||||
${this.devices.map(
|
||||
(device) => html`<paper-item>${device}</paper-item>`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
`
|
||||
: this.devices === undefined
|
||||
? this.dialogParams.supervisor.localize(
|
||||
@@ -112,11 +111,7 @@ class HassioDatadiskDialog extends LitElement {
|
||||
"dialog.datadisk_move.no_devices"
|
||||
)}
|
||||
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.cancel"
|
||||
)}
|
||||
@@ -135,8 +130,8 @@ class HassioDatadiskDialog extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _select_device(ev) {
|
||||
this.selectedDevice = ev.target.value;
|
||||
private _select_device(event) {
|
||||
this.selectedDevice = event.detail.value;
|
||||
}
|
||||
|
||||
private async _moveDatadisk() {
|
||||
@@ -161,7 +156,7 @@ class HassioDatadiskDialog extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-select {
|
||||
paper-dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
ha-circular-progress {
|
||||
|
@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/search-input";
|
||||
import "../../../../src/common/search/search-input";
|
||||
import { stringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
@@ -80,6 +80,8 @@ class HassioHardwareDialog extends LitElement {
|
||||
></ha-icon-button>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
autofocus
|
||||
no-label-float
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
@@ -176,7 +178,7 @@ class HassioHardwareDialog extends LitElement {
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
search-input {
|
||||
margin: 8px 16px 0;
|
||||
margin: 0 16px;
|
||||
display: block;
|
||||
}
|
||||
.device-property {
|
||||
|
@@ -37,10 +37,7 @@ class HassioMarkdownDialog extends LitElement {
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this.title)}
|
||||
>
|
||||
<ha-markdown
|
||||
.content=${this.content || ""}
|
||||
dialogInitialFocus
|
||||
></ha-markdown>
|
||||
<ha-markdown .content=${this.content || ""}></ha-markdown>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
@@ -119,7 +119,6 @@ export class DialogHassioNetwork
|
||||
html`<mwc-tab
|
||||
.id=${device.interface}
|
||||
.label=${device.interface}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</mwc-tab>`
|
||||
)}
|
||||
@@ -316,7 +315,6 @@ export class DialogHassioNetwork
|
||||
value="auto"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "auto"}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
|
@@ -80,7 +80,6 @@ class HassioRegistriesDialog extends LitElement {
|
||||
.schema=${SCHEMA}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabel}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
<div class="action">
|
||||
<mwc-button
|
||||
@@ -125,7 +124,7 @@ class HassioRegistriesDialog extends LitElement {
|
||||
</ha-alert>
|
||||
`}
|
||||
<div class="action">
|
||||
<mwc-button @click=${this._addRegistry} dialogInitialFocus>
|
||||
<mwc-button @click=${this._addRegistry}>
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.add_new_registry"
|
||||
)}
|
||||
|
@@ -106,9 +106,6 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
</paper-item-body>
|
||||
<div class="delete">
|
||||
<ha-icon-button
|
||||
.label=${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.remove"
|
||||
)}
|
||||
.disabled=${usedRepositories.includes(repo.slug)}
|
||||
.slug=${repo.slug}
|
||||
.path=${usedRepositories.includes(repo.slug)
|
||||
@@ -142,7 +139,6 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
"dialog.repositories.add"
|
||||
)}
|
||||
@keydown=${this._handleKeyAdd}
|
||||
dialogInitialFocus
|
||||
></paper-input>
|
||||
<mwc-button @click=${this._addRepository}>
|
||||
${this._processing
|
||||
|
@@ -1,10 +1,12 @@
|
||||
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 { customElement, property, state } from "lit/decorators";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-select";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
@@ -71,19 +73,24 @@ class HassioSupervisorLog extends LitElement {
|
||||
: ""}
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-select
|
||||
<paper-dropdown-menu
|
||||
.label=${this.supervisor.localize("system.log.log_provider")}
|
||||
@selected=${this._setLogProvider}
|
||||
.value=${this._selectedLogProvider}
|
||||
@iron-select=${this._setLogProvider}
|
||||
>
|
||||
${logProviders.map(
|
||||
(provider) => html`
|
||||
<mwc-list-item .value=${provider.key}>
|
||||
${provider.name}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="provider"
|
||||
.selected=${this._selectedLogProvider}
|
||||
>
|
||||
${logProviders.map(
|
||||
(provider) => html`
|
||||
<paper-item provider=${provider.key}>
|
||||
${provider.name}
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
`
|
||||
: ""}
|
||||
|
||||
@@ -103,7 +110,7 @@ class HassioSupervisorLog extends LitElement {
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.target.value;
|
||||
const provider = ev.detail.item.getAttribute("provider");
|
||||
this._selectedLogProvider = provider;
|
||||
this._loadData();
|
||||
}
|
||||
@@ -146,9 +153,9 @@ class HassioSupervisorLog extends LitElement {
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
paper-dropdown-menu {
|
||||
padding: 0 2%;
|
||||
width: 96%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/common/search/search-input";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
|
31
package.json
31
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"description": "A frontend for Home Assistant",
|
||||
"description": "A frontend for Home Assistant using the Polymer framework",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/home-assistant/frontend"
|
||||
"url": "https://github.com/home-assistant/home-assistant-polymer"
|
||||
},
|
||||
"name": "home-assistant-frontend",
|
||||
"version": "1.0.0",
|
||||
@@ -46,7 +46,6 @@
|
||||
"@fullcalendar/daygrid": "5.9.0",
|
||||
"@fullcalendar/interaction": "5.9.0",
|
||||
"@fullcalendar/list": "5.9.0",
|
||||
"@lit-labs/motion": "^1.0.2",
|
||||
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
|
||||
"@material/chips": "14.0.0-canary.261f2db59.0",
|
||||
"@material/data-table": "14.0.0-canary.261f2db59.0",
|
||||
@@ -96,7 +95,6 @@
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
"@vue/web-component-wrapper": "^1.2.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"app-datepicker": "^5.0.1",
|
||||
"chart.js": "^3.3.2",
|
||||
@@ -108,7 +106,7 @@
|
||||
"deep-freeze": "^0.0.1",
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.1.5",
|
||||
"hls.js": "^1.0.11",
|
||||
"home-assistant-js-websocket": "^6.0.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
@@ -117,7 +115,7 @@
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lit": "^2.1.2",
|
||||
"lit-vaadin-helpers": "^0.3.0",
|
||||
"marked": "^4.0.12",
|
||||
"marked": "^3.0.2",
|
||||
"memoize-one": "^5.2.1",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "^0.3.2",
|
||||
@@ -137,12 +135,12 @@
|
||||
"vue": "^2.6.12",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"workbox-cacheable-response": "^6.4.2",
|
||||
"workbox-core": "^6.4.2",
|
||||
"workbox-expiration": "^6.4.2",
|
||||
"workbox-precaching": "^6.4.2",
|
||||
"workbox-routing": "^6.4.2",
|
||||
"workbox-strategies": "^6.4.2",
|
||||
"workbox-cacheable-response": "^6.1.5",
|
||||
"workbox-core": "^6.1.5",
|
||||
"workbox-expiration": "^6.1.5",
|
||||
"workbox-precaching": "^6.1.5",
|
||||
"workbox-routing": "^6.1.5",
|
||||
"workbox-strategies": "^6.1.5",
|
||||
"xss": "^1.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -171,7 +169,7 @@
|
||||
"@types/js-yaml": "^4",
|
||||
"@types/leaflet": "^1",
|
||||
"@types/leaflet-draw": "^1",
|
||||
"@types/marked": "^4",
|
||||
"@types/marked": "^2",
|
||||
"@types/mocha": "^8",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"@types/sortablejs": "^1",
|
||||
@@ -198,7 +196,7 @@
|
||||
"fs-extra": "^7.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-flatmap": "^1.0.2",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
@@ -235,7 +233,7 @@
|
||||
"webpack-dev-server": "^4.3.0",
|
||||
"webpack-manifest-plugin": "^4.0.2",
|
||||
"webpackbar": "^5.0.0-3",
|
||||
"workbox-build": "^6.4.2"
|
||||
"workbox-build": "^6.1.5"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
@@ -255,6 +253,5 @@
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "always"
|
||||
},
|
||||
"packageManager": "yarn@3.2.0"
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,6 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def where() -> Path:
|
||||
def where():
|
||||
"""Return path to the frontend."""
|
||||
return Path(__file__).parent
|
||||
|
18
script/core
18
script/core
@@ -4,8 +4,6 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
WD="${WORKSPACE_DIRECTORY:=/workspaces/frontend}"
|
||||
|
||||
if [ -z "${DEVCONTAINER}" ]; then
|
||||
echo "This task should only run inside a devcontainer, for local install HA Core in a venv."
|
||||
exit 1
|
||||
@@ -18,9 +16,9 @@ if [ -z $(which hass) ]; then
|
||||
git+git://github.com/home-assistant/home-assistant.git@dev
|
||||
fi
|
||||
|
||||
if [ ! -d "${WD}/config" ]; then
|
||||
if [ ! -d "/workspaces/frontend/config" ]; then
|
||||
echo "Creating default configuration."
|
||||
mkdir -p "${WD}/config";
|
||||
mkdir -p "/workspaces/frontend/config";
|
||||
hass --script ensure_config -c config
|
||||
echo "demo:
|
||||
|
||||
@@ -28,24 +26,24 @@ logger:
|
||||
default: info
|
||||
logs:
|
||||
homeassistant.components.frontend: debug
|
||||
" >> "${WD}/config/configuration.yaml"
|
||||
" >> /workspaces/frontend/config/configuration.yaml
|
||||
|
||||
if [ ! -z "${HASSIO}" ]; then
|
||||
echo "
|
||||
# frontend:
|
||||
# development_repo: ${WD}
|
||||
# development_repo: /workspaces/frontend
|
||||
|
||||
hassio:
|
||||
development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
|
||||
development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
|
||||
else
|
||||
echo "
|
||||
frontend:
|
||||
development_repo: ${WD}
|
||||
development_repo: /workspaces/frontend
|
||||
|
||||
# hassio:
|
||||
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
|
||||
# development_repo: /workspaces/frontend" >> /workspaces/frontend/config/configuration.yaml
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
hass -c "${WD}/config"
|
||||
hass -c /workspaces/frontend/config
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220301.1
|
||||
version = 20220203.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
@@ -19,8 +19,3 @@ python_requires = >= 3.4.0
|
||||
[options.packages.find]
|
||||
include =
|
||||
hass_frontend*
|
||||
|
||||
[mypy]
|
||||
python_version = 3.4
|
||||
show_error_codes = True
|
||||
strict = True
|
||||
|
@@ -3,9 +3,9 @@ import type { ForDict } from "../../data/automation";
|
||||
|
||||
export const createDurationData = (
|
||||
duration: string | number | ForDict | undefined
|
||||
): HaDurationData | undefined => {
|
||||
): HaDurationData => {
|
||||
if (duration === undefined) {
|
||||
return undefined;
|
||||
return {};
|
||||
}
|
||||
if (typeof duration !== "object") {
|
||||
if (typeof duration === "string" || isNaN(duration)) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export const canToggleDomain = (hass: HomeAssistant, domain: string) => {
|
||||
const services = hass.services[domain];
|
||||
|
@@ -1,30 +1,14 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { canToggleDomain } from "./can_toggle_domain";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
if (domain === "group") {
|
||||
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;
|
||||
return stateObj.state === "on" || stateObj.state === "off";
|
||||
}
|
||||
|
||||
if (domain === "climate") {
|
||||
return supportsFeature(stateObj, 4096);
|
||||
}
|
||||
|
@@ -123,11 +123,7 @@ export const computeStateDisplay = (
|
||||
domain === "scene" ||
|
||||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
return formatDateTime(new Date(compareState), locale);
|
||||
} catch (_err) {
|
||||
return compareState;
|
||||
}
|
||||
return formatDateTime(new Date(compareState), locale);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -120,7 +120,6 @@ export const computeOpenIcon = (stateObj: HassEntity): string => {
|
||||
case "awning":
|
||||
case "door":
|
||||
case "gate":
|
||||
case "curtain":
|
||||
return mdiArrowExpandHorizontal;
|
||||
default:
|
||||
return mdiArrowUp;
|
||||
@@ -132,7 +131,6 @@ export const computeCloseIcon = (stateObj: HassEntity): string => {
|
||||
case "awning":
|
||||
case "door":
|
||||
case "gate":
|
||||
case "curtain":
|
||||
return mdiArrowCollapseHorizontal;
|
||||
default:
|
||||
return mdiArrowDown;
|
||||
|
@@ -1,32 +1,24 @@
|
||||
const SUFFIXES = [" ", ": "];
|
||||
|
||||
/**
|
||||
* Strips a device name from an entity name.
|
||||
* @param entityName the entity name
|
||||
* @param lowerCasedPrefix the prefix to strip, lower cased
|
||||
* @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix
|
||||
* @returns
|
||||
*/
|
||||
export const stripPrefixFromEntityName = (
|
||||
entityName: string,
|
||||
lowerCasedPrefix: string
|
||||
lowerCasedPrefixWithSpaceSuffix: string
|
||||
) => {
|
||||
const lowerCasedEntityName = entityName.toLowerCase();
|
||||
|
||||
for (const suffix of SUFFIXES) {
|
||||
const lowerCasedPrefixWithSuffix = `${lowerCasedPrefix}${suffix}`;
|
||||
|
||||
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
|
||||
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
|
||||
|
||||
// If first word already has an upper case letter (e.g. from brand name)
|
||||
// leave as-is, otherwise capitalize the first word.
|
||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||
? newName
|
||||
: newName[0].toUpperCase() + newName.slice(1);
|
||||
}
|
||||
if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length);
|
||||
|
||||
// If first word already has an upper case letter (e.g. from brand name)
|
||||
// leave as-is, otherwise capitalize the first word.
|
||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||
? newName
|
||||
: newName[0].toUpperCase() + newName.slice(1);
|
||||
};
|
||||
|
||||
const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../dom/fire_event";
|
||||
|
||||
@customElement("search-input")
|
||||
class SearchInput extends LitElement {
|
||||
@@ -14,9 +14,6 @@ class SearchInput extends LitElement {
|
||||
|
||||
@property() public filter?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public suffix = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public autofocus = false;
|
||||
|
||||
@@ -35,8 +32,8 @@ class SearchInput extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label || "Search"}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
.iconTrailing=${this.filter || this.suffix}
|
||||
.icon=${true}
|
||||
.iconTrailing=${this.filter}
|
||||
@input=${this._filterInputChanged}
|
||||
>
|
||||
<slot name="prefix" slot="leadingIcon">
|
||||
@@ -46,18 +43,16 @@ class SearchInput extends LitElement {
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
</slot>
|
||||
<div class="trailing" slot="trailingIcon">
|
||||
${this.filter &&
|
||||
html`
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
class="clear-button"
|
||||
></ha-icon-button>
|
||||
`}
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
${this.filter &&
|
||||
html`
|
||||
<ha-icon-button
|
||||
slot="trailingIcon"
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
class="clear-button"
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</ha-textfield>
|
||||
`;
|
||||
}
|
||||
@@ -86,16 +81,15 @@ class SearchInput extends LitElement {
|
||||
ha-svg-icon {
|
||||
outline: none;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
}
|
||||
.clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: inherit;
|
||||
}
|
||||
.trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
@@ -15,7 +15,6 @@ export const iconColorCSS = css`
|
||||
ha-state-icon[data-domain="media_player"][data-state="on"],
|
||||
ha-state-icon[data-domain="media_player"][data-state="paused"],
|
||||
ha-state-icon[data-domain="media_player"][data-state="playing"],
|
||||
ha-state-icon[data-domain="remote"][data-state="on"],
|
||||
ha-state-icon[data-domain="script"][data-state="on"],
|
||||
ha-state-icon[data-domain="sun"][data-state="above_horizon"],
|
||||
ha-state-icon[data-domain="switch"][data-state="on"],
|
||||
|
@@ -11,7 +11,7 @@ export const debounce = <T extends any[]>(
|
||||
immediate = false
|
||||
) => {
|
||||
let timeout: number | undefined;
|
||||
const debouncedFunc = (...args: T): void => {
|
||||
return (...args: T): void => {
|
||||
const later = () => {
|
||||
timeout = undefined;
|
||||
if (!immediate) {
|
||||
@@ -25,8 +25,4 @@ export const debounce = <T extends any[]>(
|
||||
func(...args);
|
||||
}
|
||||
};
|
||||
debouncedFunc.cancel = () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
return debouncedFunc;
|
||||
};
|
||||
|
@@ -21,7 +21,7 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../search-input";
|
||||
import "../../common/search/search-input";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
|
@@ -115,9 +115,6 @@ class DateRangePickerElement extends WrappedElement {
|
||||
color: var(--primary-text-color);
|
||||
min-width: initial !important;
|
||||
}
|
||||
.daterangepicker:after {
|
||||
display: none;
|
||||
}
|
||||
.daterangepicker:after {
|
||||
border-bottom: 6px solid var(--card-background-color);
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
deviceAutomationsEqual,
|
||||
} from "../../data/device_automation";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||
@@ -90,7 +90,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
const value = this._value;
|
||||
return html`
|
||||
<ha-select
|
||||
<mwc-select
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@selected=${this._automationChanged}
|
||||
@@ -113,7 +113,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
</mwc-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select {
|
||||
mwc-select {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
@@ -114,7 +114,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
const newValue = event.detail.value;
|
||||
if (
|
||||
newValue === curValue ||
|
||||
(newValue !== undefined && !isValidEntityId(newValue))
|
||||
(newValue !== "" && !isValidEntityId(newValue))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
ha-entity-picker {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
|
@@ -1,3 +1,6 @@
|
||||
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 { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html, TemplateResult, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-select";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "./ha-textfield";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -193,7 +193,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
: ""}
|
||||
${this.format === 24
|
||||
? ""
|
||||
: html`<ha-select
|
||||
: html`<mwc-select
|
||||
.required=${this.required}
|
||||
.value=${this.amPm}
|
||||
.disabled=${this.disabled}
|
||||
@@ -205,7 +205,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
>
|
||||
<mwc-list-item value="AM">AM</mwc-list-item>
|
||||
<mwc-list-item value="PM">PM</mwc-list-item>
|
||||
</ha-select>`}
|
||||
</mwc-select>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -280,7 +280,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
ha-textfield:last-child {
|
||||
--text-field-border-top-right-radius: var(--mdc-shape-medium);
|
||||
}
|
||||
ha-select {
|
||||
mwc-select {
|
||||
--mdc-shape-small: 0;
|
||||
width: 85px;
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "./ha-select";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -24,7 +24,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public open() {
|
||||
const select = this.shadowRoot?.querySelector("ha-select");
|
||||
const select = this.shadowRoot?.querySelector("mwc-select");
|
||||
if (select) {
|
||||
// @ts-expect-error
|
||||
select.menuOpen = true;
|
||||
@@ -49,7 +49,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-select
|
||||
<mwc-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.label")}
|
||||
fixedMenuPosition
|
||||
@@ -71,7 +71,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
</mwc-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
ha-select {
|
||||
mwc-select {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
display: block;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-menu";
|
||||
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
|
||||
import type { Corner, Menu } from "@material/mwc-menu";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
|
||||
@@ -7,12 +7,6 @@ import { customElement, property, query } from "lit/decorators";
|
||||
export class HaButtonMenu extends LitElement {
|
||||
@property() public corner: Corner = "TOP_START";
|
||||
|
||||
@property() public menuCorner: MenuCorner = "START";
|
||||
|
||||
@property({ type: Number }) public x?: number;
|
||||
|
||||
@property({ type: Number }) public y?: number;
|
||||
|
||||
@property({ type: Boolean }) public multi = false;
|
||||
|
||||
@property({ type: Boolean }) public activatable = false;
|
||||
@@ -38,12 +32,9 @@ export class HaButtonMenu extends LitElement {
|
||||
</div>
|
||||
<mwc-menu
|
||||
.corner=${this.corner}
|
||||
.menuCorner=${this.menuCorner}
|
||||
.fixed=${this.fixed}
|
||||
.multi=${this.multi}
|
||||
.activatable=${this.activatable}
|
||||
.y=${this.y}
|
||||
.x=${this.x}
|
||||
>
|
||||
<slot></slot>
|
||||
</mwc-menu>
|
||||
|
@@ -4,7 +4,6 @@ import { mdiFilterVariant } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { computeDeviceName } from "../data/device_registry";
|
||||
import { findRelated, RelatedResult } from "../data/search";
|
||||
@@ -66,7 +65,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
.fullwidth=${this.narrow}
|
||||
.corner=${this.corner}
|
||||
@closed=${this._onClosed}
|
||||
@input=${stopPropagation}
|
||||
>
|
||||
<ha-area-picker
|
||||
.label=${this.hass.localize(
|
||||
@@ -76,7 +74,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
.value=${this.value?.area}
|
||||
no-add
|
||||
@value-changed=${this._areaPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-area-picker>
|
||||
<ha-device-picker
|
||||
.label=${this.hass.localize(
|
||||
@@ -85,7 +82,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.device}
|
||||
@value-changed=${this._devicePicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-device-picker>
|
||||
<ha-entity-picker
|
||||
.label=${this.hass.localize(
|
||||
@@ -95,7 +91,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
.value=${this.value?.entity}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
@value-changed=${this._entityPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-entity-picker>
|
||||
</mwc-menu-surface>
|
||||
`;
|
||||
@@ -108,17 +103,11 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
private _onClosed(ev): void {
|
||||
ev.stopPropagation();
|
||||
private _onClosed(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _preventDefault(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private async _entityPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.detail.value;
|
||||
if (!entityId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
@@ -138,7 +127,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
}
|
||||
|
||||
private async _devicePicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const deviceId = ev.detail.value;
|
||||
if (!deviceId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
@@ -162,7 +150,6 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const areaId = ev.detail.value;
|
||||
if (!areaId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
@@ -186,7 +173,9 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
--mdc-menu-min-width: 250px;
|
||||
}
|
||||
:host([narrow]) {
|
||||
position: static;
|
||||
}
|
||||
ha-area-picker,
|
||||
ha-device-picker,
|
||||
@@ -196,15 +185,8 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
padding: 4px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-area-picker {
|
||||
padding-top: 16px;
|
||||
}
|
||||
ha-entity-picker {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
:host([narrow]) ha-area-picker,
|
||||
:host([narrow]) ha-device-picker,
|
||||
:host([narrow]) ha-entity-picker {
|
||||
:host([narrow]) ha-device-picker {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatDateNumeric } from "../common/datetime/format_date";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
|
||||
|
||||
@@ -38,17 +38,17 @@ export class HaDateInput extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
render() {
|
||||
return html`<ha-textfield
|
||||
return html`<paper-input
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
iconTrailing
|
||||
no-label-float
|
||||
@click=${this._openDialog}
|
||||
.value=${this.value
|
||||
? formatDateNumeric(new Date(this.value), this.locale)
|
||||
: ""}
|
||||
>
|
||||
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
|
||||
</ha-textfield>`;
|
||||
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
|
||||
</paper-input>`;
|
||||
}
|
||||
|
||||
private _openDialog() {
|
||||
@@ -73,6 +73,9 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
paper-input {
|
||||
width: 110px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import "@material/mwc-list/mwc-list";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -18,7 +19,6 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./date-range-picker";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface DateRangePickerRanges {
|
||||
[key: string]: [Date, Date];
|
||||
@@ -61,7 +61,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
>
|
||||
<div slot="input" class="date-range-inputs">
|
||||
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||
<ha-textfield
|
||||
<paper-input
|
||||
.value=${formatDateTime(this.startDate, this.hass.locale)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
@@ -69,16 +69,16 @@ export class HaDateRangePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleInputClick}
|
||||
readonly
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.value=${formatDateTime(this.endDate, this.hass.locale)}
|
||||
.label=${this.hass.localize(
|
||||
label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleInputClick}
|
||||
readonly
|
||||
></ha-textfield>
|
||||
></paper-input>
|
||||
</div>
|
||||
${this.ranges
|
||||
? html`<div
|
||||
@@ -158,13 +158,13 @@ export class HaDateRangePicker extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
paper-input {
|
||||
display: inline-block;
|
||||
max-width: 250px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
ha-textfield:last-child {
|
||||
paper-input:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
ha-textfield {
|
||||
paper-input {
|
||||
min-width: inherit;
|
||||
}
|
||||
|
||||
|
@@ -1,13 +1,6 @@
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
@@ -23,21 +16,11 @@ class HaExpansionPanel extends LitElement {
|
||||
|
||||
@property() secondary?: string;
|
||||
|
||||
@state() _showContent = this.expanded;
|
||||
|
||||
@query(".container") private _container!: HTMLDivElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
id="summary"
|
||||
@click=${this._toggleContainer}
|
||||
@keydown=${this._toggleContainer}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-expanded=${this.expanded}
|
||||
aria-controls="sect1"
|
||||
>
|
||||
<div class="summary" @click=${this._toggleContainer}>
|
||||
<slot class="header" name="header">
|
||||
${this.header}
|
||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||
@@ -50,37 +33,21 @@ class HaExpansionPanel extends LitElement {
|
||||
<div
|
||||
class="container ${classMap({ expanded: this.expanded })}"
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
role="region"
|
||||
aria-labelledby="summary"
|
||||
aria-hidden=${!this.expanded}
|
||||
tabindex="-1"
|
||||
>
|
||||
${this._showContent ? html`<slot></slot>` : ""}
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("expanded") && this.expanded) {
|
||||
this._showContent = this.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
this._container.style.removeProperty("height");
|
||||
this._showContent = this.expanded;
|
||||
}
|
||||
|
||||
private async _toggleContainer(ev): Promise<void> {
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
private async _toggleContainer(): Promise<void> {
|
||||
const newExpanded = !this.expanded;
|
||||
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
||||
|
||||
if (newExpanded) {
|
||||
this._showContent = true;
|
||||
// allow for dynamic content to be rendered
|
||||
await nextRender();
|
||||
}
|
||||
@@ -113,21 +80,17 @@ class HaExpansionPanel extends LitElement {
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
border-radius: var(--ha-card-border-radius, 4px);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
#summary {
|
||||
.summary {
|
||||
display: flex;
|
||||
padding: var(--expansion-panel-summary-padding, 0 8px);
|
||||
padding: var(--expansion-panel-summary-padding, 0);
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#summary:focus {
|
||||
background: var(--input-fill-color);
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
@@ -140,7 +103,6 @@ class HaExpansionPanel extends LitElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--expansion-panel-content-padding, 0 8px);
|
||||
overflow: hidden;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 0px;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import "@polymer/iron-input/iron-input";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -20,7 +21,7 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@property() public icon!: string;
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@@ -38,7 +39,15 @@ export class HaFileUpload extends LitElement {
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.autoOpenFileDialog) {
|
||||
this._openFilePicker();
|
||||
this._input?.click();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("_drag") && !this.uploading) {
|
||||
(
|
||||
this.shadowRoot!.querySelector("paper-input-container") as any
|
||||
)._setFocused(this._drag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,75 +60,51 @@ export class HaFileUpload extends LitElement {
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`
|
||||
<label
|
||||
for="input"
|
||||
class="mdc-text-field mdc-text-field--filled ${classMap({
|
||||
"mdc-text-field--focused": this._drag,
|
||||
"mdc-text-field--with-leading-icon": Boolean(this.icon),
|
||||
"mdc-text-field--with-trailing-icon": Boolean(this.value),
|
||||
})}"
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
>
|
||||
<span class="mdc-text-field__ripple"></span>
|
||||
<span
|
||||
class="mdc-floating-label ${this.value || this._drag
|
||||
? "mdc-floating-label--float-above"
|
||||
: ""}"
|
||||
id="label"
|
||||
>${this.label}</span
|
||||
<label for="input">
|
||||
<paper-input-container
|
||||
.alwaysFloatLabel=${Boolean(this.value)}
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
class=${classMap({
|
||||
dragged: this._drag,
|
||||
})}
|
||||
>
|
||||
${this.icon
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--leading"
|
||||
tabindex="-1"
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._openFilePicker}
|
||||
.path=${this.icon}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
: ""}
|
||||
<div class="value">${this.value}</div>
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="mdc-text-field__input file"
|
||||
accept=${this.accept}
|
||||
@change=${this._handleFilePicked}
|
||||
aria-labelledby="label"
|
||||
/>
|
||||
${this.value
|
||||
? html`<span
|
||||
class="mdc-text-field__icon mdc-text-field__icon--trailing"
|
||||
tabindex="1"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.close") ||
|
||||
"close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</span>`
|
||||
: ""}
|
||||
<span
|
||||
class="mdc-line-ripple ${this._drag
|
||||
? "mdc-line-ripple--active"
|
||||
: ""}"
|
||||
></span>
|
||||
<label for="input" slot="label"> ${this.label} </label>
|
||||
<iron-input slot="input">
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="file"
|
||||
accept=${this.accept}
|
||||
@change=${this._handleFilePicked}
|
||||
/>
|
||||
${this.value}
|
||||
</iron-input>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.close") ||
|
||||
"close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
slot="suffix"
|
||||
.path=${this.icon}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</paper-input-container>
|
||||
</label>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _openFilePicker() {
|
||||
this._input?.click();
|
||||
}
|
||||
|
||||
private _handleDrop(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -152,66 +137,40 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.mdc-text-field--filled {
|
||||
height: auto;
|
||||
padding-top: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon {
|
||||
padding-top: 28px;
|
||||
}
|
||||
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-text-field--filled.mdc-text-field--with-trailing-icon
|
||||
.mdc-text-field__icon {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mdc-text-field--filled .mdc-floating-label--float-above {
|
||||
transform: scale(0.75);
|
||||
top: 8px;
|
||||
}
|
||||
.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.value {
|
||||
width: 100%;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 125px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align-last: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return css`
|
||||
paper-input-container {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
paper-input-container.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
max-width: 125px;
|
||||
max-height: 125px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align-last: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,9 +9,7 @@ export class HaFormConstant extends LitElement implements HaFormElement {
|
||||
@property() public label!: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<span class="label">${this.label}</span>${this.schema.value
|
||||
? `: ${this.schema.value}`
|
||||
: ""}`;
|
||||
return html`<span class="label">${this.label}</span>: ${this.schema.value}`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,95 +0,0 @@
|
||||
import "./ha-form";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type {
|
||||
HaFormGridSchema,
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-form-grid")
|
||||
export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data!: HaFormDataContainer;
|
||||
|
||||
@property({ attribute: false }) public schema!: HaFormGridSchema;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public computeLabel?: (
|
||||
schema: HaFormSchema,
|
||||
data?: HaFormDataContainer
|
||||
) => string;
|
||||
|
||||
@property() public computeHelper?: (schema: HaFormSchema) => string;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.setAttribute("own-margin", "");
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("schema")) {
|
||||
if (this.schema.column_min_width) {
|
||||
this.style.setProperty(
|
||||
"--form-grid-min-width",
|
||||
this.schema.column_min_width
|
||||
);
|
||||
} else {
|
||||
this.style.setProperty("--form-grid-min-width", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.schema.schema.map(
|
||||
(item) =>
|
||||
html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.data}
|
||||
.schema=${[item]}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(
|
||||
var(--form-grid-column-count, auto-fit),
|
||||
minmax(var(--form-grid-min-width, 200px), 1fr)
|
||||
);
|
||||
grid-gap: 8px;
|
||||
}
|
||||
:host > ha-form {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-form-grid": HaFormGrid;
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@@ -10,8 +11,7 @@ import {
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-check-list-item";
|
||||
import type { HaCheckListItem } from "../ha-check-list-item";
|
||||
import { HaCheckListItem } from "../ha-check-list-item";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-formfield";
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import "@material/mwc-select";
|
||||
import type { Select } from "@material/mwc-select";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../ha-radio";
|
||||
import type { HaRadio } from "../ha-radio";
|
||||
import "../ha-select";
|
||||
import type { HaSelect } from "../ha-select";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
|
||||
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { HaRadio } from "../ha-radio";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public schema!: HaFormSelectSchema;
|
||||
@@ -19,7 +20,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-select", true) private _input?: HTMLElement;
|
||||
@query("mwc-select", true) private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
@@ -49,7 +50,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
<mwc-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
@@ -66,13 +67,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
<mwc-list-item .value=${value}>${label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
</mwc-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
let value: string | undefined = (ev.target as HaSelect | HaRadio).value;
|
||||
let value: string | undefined = (ev.target as Select | HaRadio).value;
|
||||
|
||||
if (value === this.data) {
|
||||
return;
|
||||
@@ -89,7 +90,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select,
|
||||
mwc-select,
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
}
|
||||
|
@@ -1,18 +1,10 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-alert";
|
||||
import "./ha-form-boolean";
|
||||
import "./ha-form-constant";
|
||||
import "./ha-form-grid";
|
||||
import "./ha-form-float";
|
||||
import "./ha-form-integer";
|
||||
import "./ha-form-multi_select";
|
||||
@@ -22,20 +14,17 @@ import "./ha-form-string";
|
||||
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
const getValue = (obj, item) =>
|
||||
obj ? (!item.name ? obj : obj[item.name]) : null;
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
const getValue = (obj, item) => (obj ? obj[item.name] : null);
|
||||
|
||||
let selectorImported = false;
|
||||
|
||||
@customElement("ha-form")
|
||||
export class HaForm extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data!: HaFormDataContainer;
|
||||
@property() public data!: HaFormDataContainer;
|
||||
|
||||
@property({ attribute: false }) public schema!: HaFormSchema[];
|
||||
@property() public schema!: HaFormSchema[];
|
||||
|
||||
@property() public error?: Record<string, string>;
|
||||
|
||||
@@ -43,12 +32,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public computeError?: (schema: HaFormSchema, error) => string;
|
||||
|
||||
@property() public computeLabel?: (
|
||||
schema: HaFormSchema,
|
||||
data?: HaFormDataContainer
|
||||
) => string;
|
||||
|
||||
@property() public computeHelper?: (schema: HaFormSchema) => string;
|
||||
@property() public computeLabel?: (schema: HaFormSchema) => string;
|
||||
|
||||
public focus() {
|
||||
const root = this.shadowRoot?.querySelector(".root");
|
||||
@@ -75,7 +59,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="root">
|
||||
${this.error && this.error.base
|
||||
@@ -86,8 +70,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
`
|
||||
: ""}
|
||||
${this.schema.map((item) => {
|
||||
const error = getError(this.error, item);
|
||||
|
||||
const error = getValue(this.error, item);
|
||||
return html`
|
||||
${error
|
||||
? html`
|
||||
@@ -102,19 +85,15 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
.hass=${this.hass}
|
||||
.selector=${item.selector}
|
||||
.value=${getValue(this.data, item)}
|
||||
.label=${this._computeLabel(item, this.data)}
|
||||
.label=${this._computeLabel(item)}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.required=${item.required || false}
|
||||
></ha-selector>`
|
||||
: dynamicElement(`ha-form-${item.type}`, {
|
||||
schema: item,
|
||||
data: getValue(this.data, item),
|
||||
label: this._computeLabel(item, this.data),
|
||||
label: this._computeLabel(item),
|
||||
disabled: this.disabled,
|
||||
hass: this.hass,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
})}
|
||||
`;
|
||||
})}
|
||||
@@ -128,30 +107,21 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
root.addEventListener("value-changed", (ev) => {
|
||||
ev.stopPropagation();
|
||||
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||
|
||||
const newValue = !schema.name
|
||||
? ev.detail.value
|
||||
: { [schema.name]: ev.detail.value };
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.data, ...newValue },
|
||||
value: { ...this.data, [schema.name]: ev.detail.value },
|
||||
});
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) {
|
||||
private _computeLabel(schema: HaFormSchema) {
|
||||
return this.computeLabel
|
||||
? this.computeLabel(schema, data)
|
||||
? this.computeLabel(schema)
|
||||
: schema
|
||||
? schema.name
|
||||
: "";
|
||||
}
|
||||
|
||||
private _computeHelper(schema: HaFormSchema) {
|
||||
return this.computeHelper ? this.computeHelper(schema) : "";
|
||||
}
|
||||
|
||||
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
|
||||
return this.computeError ? this.computeError(error, schema) : error;
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@ export type HaFormSchema =
|
||||
| HaFormSelectSchema
|
||||
| HaFormMultiSelectSchema
|
||||
| HaFormTimeSchema
|
||||
| HaFormSelector
|
||||
| HaFormGridSchema;
|
||||
| HaFormSelector;
|
||||
|
||||
export interface HaFormBaseSchema {
|
||||
name: string;
|
||||
@@ -26,13 +25,6 @@ export interface HaFormBaseSchema {
|
||||
};
|
||||
}
|
||||
|
||||
export interface HaFormGridSchema extends HaFormBaseSchema {
|
||||
type: "grid";
|
||||
name: "";
|
||||
column_min_width?: string;
|
||||
schema: HaFormSchema[];
|
||||
}
|
||||
|
||||
export interface HaFormSelector extends HaFormBaseSchema {
|
||||
type?: never;
|
||||
selector: Selector;
|
||||
@@ -40,7 +32,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
|
||||
|
||||
export interface HaFormConstantSchema extends HaFormBaseSchema {
|
||||
type: "constant";
|
||||
value?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
||||
@@ -57,7 +49,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema {
|
||||
|
||||
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
|
||||
type: "multi_select";
|
||||
options: Record<string, string> | string[] | Array<[string, string]>;
|
||||
options: Record<string, string> | string[];
|
||||
}
|
||||
|
||||
export interface HaFormFloatSchema extends HaFormBaseSchema {
|
||||
|
@@ -43,8 +43,6 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _errorIsFatal = false;
|
||||
|
||||
private _hlsPolyfillInstance?: HlsLite;
|
||||
|
||||
private _exoPlayer = false;
|
||||
@@ -55,7 +53,6 @@ class HaHLSPlayer extends LitElement {
|
||||
super.connectedCallback();
|
||||
HaHLSPlayer.streamCount += 1;
|
||||
if (this.hasUpdated) {
|
||||
this._resetError();
|
||||
this._startHls();
|
||||
}
|
||||
}
|
||||
@@ -67,23 +64,16 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`
|
||||
${this._error
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
class=${this._errorIsFatal ? "fatal" : "retry"}
|
||||
>
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${!this._errorIsFatal
|
||||
? html`<video
|
||||
?autoplay=${this.autoPlay}
|
||||
.muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
></video>`
|
||||
: ""}
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
.muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
></video>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -97,11 +87,12 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
this._startHls();
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min"))
|
||||
@@ -119,8 +110,8 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._setFatalError(
|
||||
this.hass.localize("ui.components.media-browser.video_not_supported")
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -228,16 +219,9 @@ class HaHLSPlayer extends LitElement {
|
||||
this._hlsPolyfillInstance = hls;
|
||||
hls.attachMedia(videoEl);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
this._resetError();
|
||||
hls.loadSource(url);
|
||||
});
|
||||
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.
|
||||
hls.on(Hls.Events.ERROR, (_, data: any) => {
|
||||
if (!data.fatal) {
|
||||
return;
|
||||
}
|
||||
@@ -257,22 +241,22 @@ class HaHLSPlayer extends LitElement {
|
||||
error += " (" + data.response.code + ")";
|
||||
}
|
||||
}
|
||||
this._setRetryableError(error);
|
||||
break;
|
||||
this._error = error;
|
||||
return;
|
||||
}
|
||||
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
|
||||
this._setRetryableError("Timeout while starting stream");
|
||||
break;
|
||||
this._error = "Timeout while starting stream";
|
||||
return;
|
||||
default:
|
||||
this._setRetryableError("Stream network error");
|
||||
break;
|
||||
this._error = "Unknown stream network error (" + data.details + ")";
|
||||
return;
|
||||
}
|
||||
hls.startLoad();
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
||||
this._setRetryableError("Error with media stream contents");
|
||||
hls.recoverMediaError();
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else {
|
||||
this._setFatalError("Error playing stream");
|
||||
this._error =
|
||||
"Unknown error with stream (" + data.type + ", " + data.details + ")";
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -300,21 +284,6 @@ 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 {
|
||||
return css`
|
||||
:host,
|
||||
@@ -327,14 +296,10 @@ class HaHLSPlayer extends LitElement {
|
||||
max-height: var(--video-max-height, calc(100vh - 97px));
|
||||
}
|
||||
|
||||
.fatal {
|
||||
ha-alert {
|
||||
display: block;
|
||||
padding: 100px 16px;
|
||||
}
|
||||
|
||||
.retry {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
28
src/components/ha-paper-dropdown-menu.ts
Normal file
28
src/components/ha-paper-dropdown-menu.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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);
|
@@ -1,4 +1,5 @@
|
||||
import { mdiImagePlus } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import { mdiCamera } from "@mdi/js";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
|
@@ -1,57 +0,0 @@
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { styles } from "@material/mwc-select/mwc-select.css";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
|
||||
@customElement("ha-select")
|
||||
export class HaSelect extends SelectBase {
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon?: boolean;
|
||||
|
||||
protected override renderLeadingIcon() {
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<span class="mdc-select__icon"
|
||||
><slot name="icon"></slot
|
||||
></span>`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
"translations-updated",
|
||||
this._translationsUpdated
|
||||
);
|
||||
}
|
||||
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-select": HaSelect;
|
||||
}
|
||||
}
|
@@ -35,12 +35,9 @@ export class HaBooleanSelector extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
}
|
||||
ha-formfield {
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
--mdc-typography-body2-font-size: 1em;
|
||||
}
|
||||
`;
|
||||
|
@@ -50,13 +50,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (this.selector.entity?.domain) {
|
||||
const filterDomain = this.selector.entity.domain;
|
||||
const filterDomainIsArray = Array.isArray(filterDomain);
|
||||
const entityDomain = computeStateDomain(entity);
|
||||
if (
|
||||
(filterDomainIsArray && !filterDomain.includes(entityDomain)) ||
|
||||
(!filterDomainIsArray && entityDomain !== filterDomain)
|
||||
) {
|
||||
if (computeStateDomain(entity) !== this.selector.entity.domain) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,41 +0,0 @@
|
||||
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}
|
||||
.fallbackPath=${this.selector.icon.fallbackPath}
|
||||
.placeholder=${this.selector.icon.placeholder}
|
||||
@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;
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocationSelector,
|
||||
LocationSelectorValue,
|
||||
} from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { MarkerLocation } from "../map/ha-locations-editor";
|
||||
import "../map/ha-locations-editor";
|
||||
|
||||
@customElement("ha-selector-location")
|
||||
export class HaLocationSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: LocationSelector;
|
||||
|
||||
@property() public value?: LocationSelectorValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._location(this.selector, this.value)}
|
||||
@location-updated=${this._locationChanged}
|
||||
@radius-updated=${this._radiusChanged}
|
||||
></ha-locations-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _location = memoizeOne(
|
||||
(
|
||||
selector: LocationSelector,
|
||||
value?: LocationSelectorValue
|
||||
): MarkerLocation[] => {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const zoneRadiusColor = selector.location.radius
|
||||
? computedStyles.getPropertyValue("--zone-radius-color") ||
|
||||
computedStyles.getPropertyValue("--accent-color")
|
||||
: undefined;
|
||||
return [
|
||||
{
|
||||
id: "location",
|
||||
latitude: value?.latitude || this.hass.config.latitude,
|
||||
longitude: value?.longitude || this.hass.config.longitude,
|
||||
radius: selector.location.radius ? value?.radius || 1000 : undefined,
|
||||
radius_color: zoneRadiusColor,
|
||||
icon: selector.location.icon,
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
const [latitude, longitude] = ev.detail.location;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.value, latitude, longitude },
|
||||
});
|
||||
}
|
||||
|
||||
private _radiusChanged(ev: CustomEvent) {
|
||||
const radius = ev.detail.radius;
|
||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-location": HaLocationSelector;
|
||||
}
|
||||
}
|
@@ -1,277 +0,0 @@
|
||||
import { mdiPlayBox, mdiPlus } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import {
|
||||
MediaClassBrowserSettings,
|
||||
MediaPickedEvent,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../ha-form/types";
|
||||
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
|
||||
|
||||
const MANUAL_SCHEMA = [
|
||||
{ name: "media_content_id", required: false, selector: { text: {} } },
|
||||
{ name: "media_content_type", required: false, selector: { text: {} } },
|
||||
];
|
||||
|
||||
@customElement("ha-selector-media")
|
||||
export class HaMediaSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: MediaSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: MediaSelectorValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@state() private _thumbnailUrl?: string | null;
|
||||
|
||||
willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("value")) {
|
||||
const thumbnail = this.value?.metadata?.thumbnail;
|
||||
const oldThumbnail = (changedProps.get("value") as this["value"])
|
||||
?.metadata?.thumbnail;
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else if (
|
||||
thumbnail &&
|
||||
thumbnail.startsWith("https://brands.home-assistant.io")
|
||||
) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
this._thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnail),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.value?.entity_id
|
||||
? this.hass.states[this.value.entity_id]
|
||||
: undefined;
|
||||
|
||||
const supportsBrowse =
|
||||
!this.value?.entity_id ||
|
||||
(stateObj && supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA));
|
||||
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.entity_id}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
|
||||
.disabled=${this.disabled}
|
||||
include-domains='["media_player"]'
|
||||
allow-custom-entity
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
${!supportsBrowse
|
||||
? html`<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.components.selectors.media.browse_not_supported"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.value}
|
||||
.schema=${MANUAL_SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
></ha-form>`
|
||||
: html`<ha-card
|
||||
outlined
|
||||
@click=${this._pickMedia}
|
||||
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""}
|
||||
>
|
||||
<div
|
||||
class="thumbnail ${classMap({
|
||||
portrait:
|
||||
!!this.value?.metadata?.media_class &&
|
||||
MediaClassBrowserSettings[
|
||||
this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
].thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${this.value?.metadata?.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="${classMap({
|
||||
"centered-image":
|
||||
!!this.value.metadata.media_class &&
|
||||
["app", "directory"].includes(
|
||||
this.value.metadata.media_class
|
||||
),
|
||||
})}
|
||||
image"
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${!this.value?.media_content_id
|
||||
? mdiPlus
|
||||
: this.value?.metadata?.media_class
|
||||
? MediaClassBrowserSettings[
|
||||
this.value.metadata.media_class === "directory"
|
||||
? this.value.metadata.children_media_class ||
|
||||
this.value.metadata.media_class
|
||||
: this.value.metadata.media_class
|
||||
].icon
|
||||
: mdiPlayBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="title">
|
||||
${!this.value?.media_content_id
|
||||
? this.hass.localize("ui.components.selectors.media.pick_media")
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
</div>
|
||||
</ha-card>`}`;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${schema.name}`);
|
||||
|
||||
private _entityChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
entity_id: ev.detail.value,
|
||||
media_content_id: "",
|
||||
media_content_type: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _pickMedia() {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "pick",
|
||||
entityId: this.value!.entity_id!,
|
||||
navigateIds: this.value!.metadata?.navigateIds,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
media_content_id: pickedMedia.item.media_content_id,
|
||||
media_content_type: pickedMedia.item.media_content_type,
|
||||
metadata: {
|
||||
title: pickedMedia.item.title,
|
||||
thumbnail: pickedMedia.item.thumbnail,
|
||||
media_class: pickedMedia.item.media_class,
|
||||
children_media_class: pickedMedia.item.children_media_class,
|
||||
navigateIds: pickedMedia.navigateIds?.map((id) => ({
|
||||
media_content_type: id.media_content_type,
|
||||
media_content_id: id.media_content_id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-card.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
ha-card .thumbnail {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transition: padding-bottom 0.1s ease-out;
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
ha-card .thumbnail.portrait {
|
||||
padding-bottom: 150%;
|
||||
}
|
||||
ha-card .image {
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.folder {
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
padding-top: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.centered-image {
|
||||
margin: 0 8px;
|
||||
background-size: contain;
|
||||
}
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-media": HaMediaSelector;
|
||||
}
|
||||
}
|
@@ -19,24 +19,22 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`${this.selector.number.mode !== "box"
|
||||
? html`${this.label}<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>`
|
||||
return html`${this.label}
|
||||
${this.selector.number.mode !== "box"
|
||||
? html`<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
inputMode="numeric"
|
||||
@@ -46,10 +44,9 @@ export class HaNumberSelector extends LitElement {
|
||||
class=${classMap({ single: this.selector.number.mode === "box" })}
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this.value || ""}
|
||||
.value=${this.value}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.suffix=${this.selector.number.unit_of_measurement}
|
||||
type="number"
|
||||
autoValidate
|
||||
@@ -60,16 +57,14 @@ export class HaNumberSelector extends LitElement {
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value ?? (this.selector.number.min || 0);
|
||||
return this.value ?? 0;
|
||||
}
|
||||
|
||||
private _handleInputChange(ev) {
|
||||
ev.stopPropagation();
|
||||
const value =
|
||||
ev.target.value === "" || isNaN(ev.target.value)
|
||||
? this.required
|
||||
? this.selector.number.min || 0
|
||||
: undefined
|
||||
? undefined
|
||||
: Number(ev.target.value);
|
||||
if (this.value === value) {
|
||||
return;
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import { SelectSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@@ -17,28 +17,24 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-select
|
||||
return html`<mwc-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
|
||||
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
|
||||
})}
|
||||
</ha-select>`;
|
||||
${this.selector.select.options.map(
|
||||
(item: string) => html`
|
||||
<mwc-list-item .value=${item}>${item}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-select>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
@@ -53,7 +49,7 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select {
|
||||
mwc-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
@@ -69,14 +69,10 @@ export class HaTextSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
let value = ev.target.value;
|
||||
const value = ev.target.value;
|
||||
if (this.value === value) {
|
||||
return;
|
||||
}
|
||||
if (value === "" && !this.required) {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
|
@@ -1,34 +0,0 @@
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ThemeSelector } from "../../data/selector";
|
||||
|
||||
@customElement("ha-selector-theme")
|
||||
export class HaThemeSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ThemeSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hui-theme-select-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
></hui-theme-select-editor>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-theme": HaThemeSelector;
|
||||
}
|
||||
}
|
@@ -22,7 +22,6 @@ export class HaTimeSelector extends LitElement {
|
||||
.value=${this.value}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label}
|
||||
enable-second
|
||||
></ha-time-input>
|
||||
`;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { Selector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { Selector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-selector-action";
|
||||
import "./ha-selector-addon";
|
||||
import "./ha-selector-area";
|
||||
@@ -17,10 +17,6 @@ import "./ha-selector-select";
|
||||
import "./ha-selector-target";
|
||||
import "./ha-selector-text";
|
||||
import "./ha-selector-time";
|
||||
import "./ha-selector-icon";
|
||||
import "./ha-selector-media";
|
||||
import "./ha-selector-theme";
|
||||
import "./ha-selector-location";
|
||||
|
||||
@customElement("ha-selector")
|
||||
export class HaSelector extends LitElement {
|
||||
@@ -32,8 +28,6 @@ export class HaSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: any;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -58,7 +52,6 @@ export class HaSelector extends LitElement {
|
||||
placeholder: this.placeholder,
|
||||
disabled: this.disabled,
|
||||
required: this.required,
|
||||
helper: this.helper,
|
||||
id: "selector",
|
||||
})}
|
||||
`;
|
||||
|
@@ -489,6 +489,9 @@ export class HaServiceControl extends LitElement {
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row paper-input {
|
||||
width: 60%;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 60%;
|
||||
}
|
||||
|
@@ -9,12 +9,6 @@ export class HaTextField extends TextFieldBase {
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon?: boolean;
|
||||
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public iconTrailing?: boolean;
|
||||
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
@@ -59,15 +53,6 @@ export class HaTextField extends TextFieldBase {
|
||||
padding-right: var(--text-field-suffix-padding-right, 0px);
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||
.mdc-text-field__affix--suffix {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.mdc-text-field__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: var(--text-field-text-align);
|
||||
}
|
||||
@@ -83,14 +68,6 @@ export class HaTextField extends TextFieldBase {
|
||||
:host([no-spinner]) input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.mdc-text-field__ripple {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mdc-text-field {
|
||||
overflow: var(--text-field-overflow);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
private _remoteStream?: MediaStream;
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
@@ -58,19 +58,12 @@ class HaWebRtcPlayer extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._startWebRtc();
|
||||
}
|
||||
}
|
||||
|
||||
public override disconnectedCallback() {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._cleanUp();
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues<this>) {
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("entityid")) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,337 +0,0 @@
|
||||
import { animate } from "@lit-labs/motion";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiClose, mdiDelete } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import {
|
||||
MediaClassBrowserSettings,
|
||||
MediaPlayerItem,
|
||||
} from "../../data/media-player";
|
||||
import {
|
||||
browseLocalMediaPlayer,
|
||||
removeLocalMedia,
|
||||
} from "../../data/media_source";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-dialog";
|
||||
import "../ha-header-bar";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-check-list-item";
|
||||
import "./ha-media-player-browse";
|
||||
import "./ha-media-upload-button";
|
||||
import type { MediaManageDialogParams } from "./show-media-manage-dialog";
|
||||
|
||||
@customElement("dialog-media-manage")
|
||||
class DialogMediaManage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _currentItem?: MediaPlayerItem;
|
||||
|
||||
@state() private _params?: MediaManageDialogParams;
|
||||
|
||||
@state() private _uploading = false;
|
||||
|
||||
@state() private _deleting = false;
|
||||
|
||||
@state() private _selected = new Set<number>();
|
||||
|
||||
private _filesChanged = false;
|
||||
|
||||
public showDialog(params: MediaManageDialogParams): void {
|
||||
this._params = params;
|
||||
this._refreshMedia();
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (this._filesChanged && this._params!.onClose) {
|
||||
this._params!.onClose();
|
||||
}
|
||||
this._params = undefined;
|
||||
this._currentItem = undefined;
|
||||
this._uploading = false;
|
||||
this._deleting = false;
|
||||
this._filesChanged = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const children =
|
||||
this._currentItem?.children?.filter((child) => !child.can_expand) || [];
|
||||
|
||||
let fileIndex = 0;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
.heading=${this._params.currentItem.title}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-header-bar slot="heading">
|
||||
${this._selected.size === 0
|
||||
? html`
|
||||
<span slot="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.title"
|
||||
)}
|
||||
</span>
|
||||
|
||||
<ha-media-upload-button
|
||||
.disabled=${this._deleting}
|
||||
.hass=${this.hass}
|
||||
.currentItem=${this._params.currentItem}
|
||||
@uploading=${this._startUploading}
|
||||
@media-refresh=${this._doneUploading}
|
||||
slot="actionItems"
|
||||
></ha-media-upload-button>
|
||||
${this._uploading
|
||||
? ""
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
slot="actionItems"
|
||||
class="header_button"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
class="danger"
|
||||
slot="title"
|
||||
.disabled=${this._deleting}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.file_management.${
|
||||
this._deleting ? "deleting" : "delete"
|
||||
}`,
|
||||
{ count: this._selected.size }
|
||||
)}
|
||||
@click=${this._handleDelete}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
|
||||
</mwc-button>
|
||||
|
||||
${this._deleting
|
||||
? ""
|
||||
: html`
|
||||
<mwc-button
|
||||
slot="actionItems"
|
||||
.label=${`Deselect all`}
|
||||
@click=${this._handleDeselectAll}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiClose}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
</mwc-button>
|
||||
`}
|
||||
`}
|
||||
</ha-header-bar>
|
||||
${!this._currentItem
|
||||
? html`
|
||||
<div class="refresh">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
`
|
||||
: !children.length
|
||||
? html`<div class="no-items">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.no_items"
|
||||
)}
|
||||
</p>
|
||||
${this._currentItem?.children?.length
|
||||
? html`<span class="folders"
|
||||
>${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.folders_not_supported"
|
||||
)}</span
|
||||
>`
|
||||
: ""}
|
||||
</div>`
|
||||
: html`
|
||||
<mwc-list multi @selected=${this._handleSelected}>
|
||||
${repeat(
|
||||
children,
|
||||
(item) => item.media_content_id,
|
||||
(item) => {
|
||||
const icon = html`
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${MediaClassBrowserSettings[
|
||||
item.media_class === "directory"
|
||||
? item.children_media_class || item.media_class
|
||||
: item.media_class
|
||||
].icon}
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
${animate({
|
||||
id: item.media_content_id,
|
||||
skipInitial: true,
|
||||
})}
|
||||
graphic="icon"
|
||||
.disabled=${this._uploading || this._deleting}
|
||||
.selected=${this._selected.has(fileIndex++)}
|
||||
.item=${item}
|
||||
>
|
||||
${icon} ${item.title}
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</mwc-list>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSelected(ev) {
|
||||
this._selected = ev.detail.index;
|
||||
}
|
||||
|
||||
private _startUploading() {
|
||||
this._uploading = true;
|
||||
this._filesChanged = true;
|
||||
}
|
||||
|
||||
private _doneUploading() {
|
||||
this._uploading = false;
|
||||
this._refreshMedia();
|
||||
}
|
||||
|
||||
private _handleDeselectAll() {
|
||||
if (this._selected.size) {
|
||||
this._selected = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleDelete() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.components.media-browser.file_management.confirm_delete",
|
||||
{ count: this._selected.size }
|
||||
),
|
||||
warning: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._filesChanged = true;
|
||||
this._deleting = true;
|
||||
|
||||
const toDelete: MediaPlayerItem[] = [];
|
||||
let fileIndex = 0;
|
||||
this._currentItem!.children!.forEach((item) => {
|
||||
if (item.can_expand) {
|
||||
return;
|
||||
}
|
||||
if (this._selected.has(fileIndex++)) {
|
||||
toDelete.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
toDelete.map(async (item) => {
|
||||
await removeLocalMedia(this.hass, item.media_content_id);
|
||||
this._currentItem = {
|
||||
...this._currentItem!,
|
||||
children: this._currentItem!.children!.filter((i) => i !== item),
|
||||
};
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
this._deleting = false;
|
||||
this._selected = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
private async _refreshMedia() {
|
||||
this._selected = new Set();
|
||||
this._currentItem = undefined;
|
||||
this._currentItem = await browseLocalMediaPlayer(
|
||||
this.hass,
|
||||
this._params!.currentItem.media_content_id
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 8;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-height: calc(100vh - 72px);
|
||||
}
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
ha-media-upload-button,
|
||||
mwc-button {
|
||||
--mdc-theme-primary: var(--mdc-theme-on-primary);
|
||||
}
|
||||
|
||||
.danger {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
ha-svg-icon[slot="icon"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
display: flex;
|
||||
height: 200px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
}
|
||||
.folders {
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-media-manage": DialogMediaManage;
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import "../ha-header-bar";
|
||||
import { mdiArrowLeft, mdiClose } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import type {
|
||||
@@ -13,11 +13,7 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog";
|
||||
import "./ha-media-player-browse";
|
||||
import "./ha-media-manage-button";
|
||||
import type {
|
||||
HaMediaPlayerBrowse,
|
||||
MediaPlayerItemId,
|
||||
} from "./ha-media-player-browse";
|
||||
import type { MediaPlayerItemId } from "./ha-media-player-browse";
|
||||
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
|
||||
|
||||
@customElement("dialog-media-player-browse")
|
||||
@@ -30,14 +26,12 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@state() private _params?: MediaPlayerBrowseDialogParams;
|
||||
|
||||
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
|
||||
|
||||
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||
this._params = params;
|
||||
this._navigateIds = params.navigateIds || [
|
||||
this._navigateIds = [
|
||||
{
|
||||
media_content_id: undefined,
|
||||
media_content_type: undefined,
|
||||
media_content_id: this._params.mediaContentId,
|
||||
media_content_type: this._params.mediaContentType,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -86,12 +80,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
: this._currentItem.title}
|
||||
</span>
|
||||
|
||||
<ha-media-manage-button
|
||||
slot="actionItems"
|
||||
.hass=${this.hass}
|
||||
.currentItem=${this._currentItem}
|
||||
@media-refresh=${this._refreshMedia}
|
||||
></ha-media-manage-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
@@ -136,10 +124,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
return this._params!.action || "play";
|
||||
}
|
||||
|
||||
private _refreshMedia() {
|
||||
this._browser.refresh();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
@@ -173,10 +157,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
ha-media-manage-button {
|
||||
--mdc-theme-primary: var(--mdc-theme-on-primary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,280 +0,0 @@
|
||||
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 { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { fetchCloudStatus, updateCloudPref } from "../../data/cloud";
|
||||
import {
|
||||
CloudTTSInfo,
|
||||
getCloudTTSInfo,
|
||||
getCloudTtsLanguages,
|
||||
getCloudTtsSupportedGenders,
|
||||
} from "../../data/cloud/tts";
|
||||
import {
|
||||
MediaPlayerBrowseAction,
|
||||
MediaPlayerItem,
|
||||
} from "../../data/media-player";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { buttonLinkStyle } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "../ha-textarea";
|
||||
|
||||
export interface TtsMediaPickedEvent {
|
||||
item: MediaPlayerItem;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"tts-picked": TtsMediaPickedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@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", true, false) private _message!: string;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-textarea
|
||||
autogrow
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.tts.message"
|
||||
)}
|
||||
.value=${this._message ||
|
||||
this.hass.localize(
|
||||
"ui.components.media-browser.tts.example_message",
|
||||
{
|
||||
name: this.hass.user?.name || "",
|
||||
}
|
||||
)}
|
||||
>
|
||||
</ha-textarea>
|
||||
${this._cloudDefaultOptions ? this._renderCloudOptions() : ""}
|
||||
</div>
|
||||
<div class="card-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.components.media-browser.tts.set_as_default"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: html`<span></span>`}
|
||||
|
||||
<mwc-button @click=${this._ttsClicked}>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.tts.action_${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card> `;
|
||||
}
|
||||
|
||||
private _renderCloudOptions() {
|
||||
if (!this._cloudTTSInfo || !this._cloudOptions) {
|
||||
return "";
|
||||
}
|
||||
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">
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.tts.language"
|
||||
)}
|
||||
.value=${selectedVoice[0]}
|
||||
@selected=${this._handleLanguageChange}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${languages.map(
|
||||
([key, label]) =>
|
||||
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.hass.localize("ui.components.media-browser.tts.gender")}
|
||||
.value=${selectedVoice[1]}
|
||||
@selected=${this._handleGenderChange}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${genders.map(
|
||||
([key, label]) =>
|
||||
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected override willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("item")) {
|
||||
if (this.item.media_content_id) {
|
||||
const params = new URLSearchParams(
|
||||
this.item.media_content_id.split("?")[1]
|
||||
);
|
||||
const message = params.get("message");
|
||||
const language = params.get("language");
|
||||
const gender = params.get("gender");
|
||||
if (message) {
|
||||
this._message = message;
|
||||
}
|
||||
if (language && gender) {
|
||||
this._cloudOptions = [language, gender];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (!this._cloudOptions) {
|
||||
this._cloudOptions = { ...this._cloudDefaultOptions };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
private getLanguages = memoizeOne(getCloudTtsLanguages);
|
||||
|
||||
private getSupportedGenders = memoizeOne(getCloudTtsSupportedGenders);
|
||||
|
||||
private get isCloudItem(): boolean {
|
||||
return this.item.media_content_id.startsWith("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 = `${
|
||||
item.media_content_id.split("?")[0]
|
||||
}?${query.toString()}`;
|
||||
item.can_play = true;
|
||||
item.title = message;
|
||||
fireEvent(this, "tts-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.components.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 ha-select {
|
||||
width: 48%;
|
||||
}
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-browse-media-tts": BrowseMediaTTS;
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
import { mdiFolderEdit } from "@mdi/js";
|
||||
import "@material/mwc-button";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { MediaPlayerItem } from "../../data/media-player";
|
||||
import "../ha-svg-icon";
|
||||
import { isLocalMediaSourceContentId } from "../../data/media_source";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showMediaManageDialog } from "./show-media-manage-dialog";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"media-refresh": unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-media-manage-button")
|
||||
class MediaManageButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() currentItem?: MediaPlayerItem;
|
||||
|
||||
@state() _uploading = 0;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.currentItem ||
|
||||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<mwc-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.manage"
|
||||
)}
|
||||
@click=${this._manage}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiFolderEdit} slot="icon"></ha-svg-icon>
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _manage() {
|
||||
showMediaManageDialog(this, {
|
||||
currentItem: this.currentItem!,
|
||||
onClose: () => fireEvent(this, "media-refresh"),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
mwc-button {
|
||||
/* We use icon + text to show disabled state */
|
||||
--mdc-button-disabled-ink-color: --mdc-theme-primary;
|
||||
}
|
||||
|
||||
ha-svg-icon[slot="icon"],
|
||||
ha-circular-progress[slot="icon"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-manage-button": MediaManageButton;
|
||||
}
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import { mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
@@ -34,36 +35,25 @@ import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import { browseLocalMediaPlayer } from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import type { HaCard } from "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-fab";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-browse-media-tts";
|
||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
import "../ha-fab";
|
||||
import { browseLocalMediaPlayer } from "../../data/media_source";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"media-picked": MediaPickedEvent;
|
||||
"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;
|
||||
};
|
||||
"media-browsed": { ids: MediaPlayerItemId[]; current?: MediaPlayerItem };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,11 +122,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
currentId.media_content_id,
|
||||
currentId.media_content_type
|
||||
);
|
||||
// Update the parent with latest item.
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: this.navigateIds,
|
||||
current: this._currentItem,
|
||||
});
|
||||
} catch (err) {
|
||||
this._setError(err);
|
||||
}
|
||||
@@ -164,11 +149,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
const subtitle = this.hass.localize(
|
||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||
);
|
||||
const children = currentItem.children || [];
|
||||
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||||
const childrenMediaClass = currentItem.children_media_class
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
const childrenMediaClass =
|
||||
MediaClassBrowserSettings[currentItem.children_media_class];
|
||||
|
||||
return html`
|
||||
${
|
||||
@@ -262,191 +246,159 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
${this._renderError(this._error)}
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !children.length && !currentItem.not_shown
|
||||
? html`
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: childrenMediaClass.layout === "grid"
|
||||
? html`
|
||||
<div
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${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>
|
||||
`
|
||||
)}
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${children.map(
|
||||
(child) => html`
|
||||
<mwc-list-item
|
||||
@click=${this._childClicked}
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
: currentItem.children?.length
|
||||
? childrenMediaClass.layout === "grid"
|
||||
? html`
|
||||
<div
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait",
|
||||
})}"
|
||||
>
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<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"
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._childClicked}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<span class="title">${child.title}</span>
|
||||
</mwc-list-item>
|
||||
<li divider role="separator"></li>
|
||||
`
|
||||
)}
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<mwc-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
@click=${this._childClicked}
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<span class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
<div
|
||||
class=${classMap({
|
||||
graphic: true,
|
||||
lazythumbnail:
|
||||
mediaClass.show_list_images === true,
|
||||
})}
|
||||
data-src=${ifDefined(
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? child.thumbnail
|
||||
: undefined
|
||||
)}
|
||||
</span>
|
||||
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">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
<br />
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`<br />${this.hass.localize(
|
||||
"ui.components.media-browser.learn_adding_local_media",
|
||||
"documentation",
|
||||
html`<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/more-info/local-media/add-media"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.components.media-browser.documentation"
|
||||
)}</a
|
||||
>`
|
||||
)}
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.local_media_files"
|
||||
)}`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
@@ -473,8 +425,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
if (changedProps.has("entityId")) {
|
||||
this._setError(undefined);
|
||||
} else if (!changedProps.has("navigateIds")) {
|
||||
// Neither entity ID or navigateIDs changed, nothing to fetch
|
||||
}
|
||||
if (!changedProps.has("navigateIds")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -483,7 +435,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
const oldNavigateIds = changedProps.get("navigateIds") as
|
||||
| this["navigateIds"]
|
||||
| undefined;
|
||||
const navigateIds = this.navigateIds;
|
||||
|
||||
// We're navigating. Reset the shizzle.
|
||||
this._content?.scrollTo(0, 0);
|
||||
@@ -492,9 +443,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
const oldParentItem = this._parentItem;
|
||||
this._currentItem = undefined;
|
||||
this._parentItem = undefined;
|
||||
const currentId = navigateIds[navigateIds.length - 1];
|
||||
const currentId = this.navigateIds[this.navigateIds.length - 1];
|
||||
const parentId =
|
||||
navigateIds.length > 1 ? navigateIds[navigateIds.length - 2] : undefined;
|
||||
this.navigateIds.length > 1
|
||||
? this.navigateIds[this.navigateIds.length - 2]
|
||||
: undefined;
|
||||
let currentProm: Promise<MediaPlayerItem> | undefined;
|
||||
let parentProm: Promise<MediaPlayerItem> | undefined;
|
||||
|
||||
@@ -503,9 +456,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
if (
|
||||
// Check if we navigated to a child
|
||||
oldNavigateIds &&
|
||||
navigateIds.length === oldNavigateIds.length + 1 &&
|
||||
this.navigateIds.length > oldNavigateIds.length &&
|
||||
oldNavigateIds.every((oldVal, idx) => {
|
||||
const curVal = navigateIds[idx];
|
||||
const curVal = this.navigateIds[idx];
|
||||
return (
|
||||
curVal.media_content_id === oldVal.media_content_id &&
|
||||
curVal.media_content_type === oldVal.media_content_type
|
||||
@@ -516,8 +469,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
} else if (
|
||||
// Check if we navigated to a parent
|
||||
oldNavigateIds &&
|
||||
navigateIds.length === oldNavigateIds.length - 1 &&
|
||||
navigateIds.every((curVal, idx) => {
|
||||
this.navigateIds.length < oldNavigateIds.length &&
|
||||
this.navigateIds.every((curVal, idx) => {
|
||||
const oldVal = oldNavigateIds[idx];
|
||||
return (
|
||||
curVal.media_content_id === oldVal.media_content_id &&
|
||||
@@ -540,33 +493,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
(item) => {
|
||||
this._currentItem = item;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
ids: this.navigateIds,
|
||||
current: item,
|
||||
});
|
||||
},
|
||||
(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);
|
||||
}
|
||||
}
|
||||
(err) => this._setError(err)
|
||||
);
|
||||
// Fetch parent
|
||||
if (!parentProm && parentId !== undefined) {
|
||||
@@ -602,17 +533,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
||||
}
|
||||
|
||||
private _ttsPicked(ev: CustomEvent<TtsMediaPickedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
const navigateIds = this.navigateIds.slice(0, -1);
|
||||
navigateIds.push(ev.detail.item);
|
||||
fireEvent(this, "media-picked", {
|
||||
...ev.detail,
|
||||
navigateIds,
|
||||
});
|
||||
fireEvent(this, "media-picked", { item });
|
||||
}
|
||||
|
||||
private async _childClicked(ev: MouseEvent): Promise<void> {
|
||||
@@ -682,17 +603,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
// Thumbnails served by local API require authentication
|
||||
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
|
||||
thumbnailUrl = signedPath.path;
|
||||
} else if (
|
||||
thumbnailUrl.startsWith("https://brands.home-assistant.io")
|
||||
) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
|
||||
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
|
||||
@@ -826,18 +736,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
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 {
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
@@ -923,17 +821,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
transition: height 0.5s, margin 0.5s;
|
||||
}
|
||||
|
||||
.not-shown {
|
||||
font-style: italic;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.grid.not-shown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ============= CHILDREN ============= */
|
||||
|
||||
mwc-list {
|
||||
|
@@ -1,129 +0,0 @@
|
||||
import { mdiUpload } from "@mdi/js";
|
||||
import "@material/mwc-button";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { MediaPlayerItem } from "../../data/media-player";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-svg-icon";
|
||||
import {
|
||||
isLocalMediaSourceContentId,
|
||||
uploadLocalMedia,
|
||||
} from "../../data/media_source";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
uploading: unknown;
|
||||
"media-refresh": unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-media-upload-button")
|
||||
class MediaUploadButton extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() currentItem?: MediaPlayerItem;
|
||||
|
||||
@state() _uploading = 0;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.currentItem ||
|
||||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<mwc-button
|
||||
.label=${this._uploading > 0
|
||||
? 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}
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _startUpload() {
|
||||
if (this._uploading > 0) {
|
||||
return;
|
||||
}
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "audio/*,video/*,image/*";
|
||||
input.multiple = true;
|
||||
input.addEventListener(
|
||||
"change",
|
||||
async () => {
|
||||
fireEvent(this, "uploading");
|
||||
const files = input.files!;
|
||||
document.body.removeChild(input);
|
||||
const target = this.currentItem!.media_content_id!;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
this._uploading = files.length - i;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await uploadLocalMedia(this.hass, target, files[i]);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.components.media-browser.file_management.upload_failed",
|
||||
{
|
||||
reason: err.message || err,
|
||||
}
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._uploading = 0;
|
||||
fireEvent(this, "media-refresh");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
// https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only
|
||||
input.style.display = "none";
|
||||
document.body.append(input);
|
||||
input.click();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
mwc-button {
|
||||
/* We use icon + text to show disabled state */
|
||||
--mdc-button-disabled-ink-color: --mdc-theme-primary;
|
||||
}
|
||||
|
||||
ha-svg-icon[slot="icon"],
|
||||
ha-circular-progress[slot="icon"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-upload-button": MediaUploadButton;
|
||||
}
|
||||
}
|
@@ -3,13 +3,13 @@ import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import { MediaPlayerItemId } from "./ha-media-player-browse";
|
||||
|
||||
export interface MediaPlayerBrowseDialogParams {
|
||||
action: MediaPlayerBrowseAction;
|
||||
entityId: string;
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||
navigateIds?: MediaPlayerItemId[];
|
||||
mediaContentId?: string;
|
||||
mediaContentType?: string;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
|
@@ -1,18 +0,0 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { MediaPlayerItem } from "../../data/media-player";
|
||||
|
||||
export interface MediaManageDialogParams {
|
||||
currentItem: MediaPlayerItem;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const showMediaManageDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: MediaManageDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-media-manage",
|
||||
dialogImport: () => import("./dialog-media-manage"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -6,8 +5,9 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { fetchUsers, User } from "../../data/user";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "./ha-user-badge";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
|
||||
class HaUserPicker extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
@@ -34,7 +34,7 @@ class HaUserPicker extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-select
|
||||
<mwc-select
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
@@ -58,7 +58,7 @@ class HaUserPicker extends LitElement {
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
</mwc-select>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -47,7 +47,6 @@ export interface CloudPreferences {
|
||||
export interface CloudStatusLoggedIn {
|
||||
logged_in: true;
|
||||
cloud: "disconnected" | "connecting" | "connected";
|
||||
cloud_last_disconnect_reason: { clean: boolean; reason: string } | null;
|
||||
email: string;
|
||||
google_registered: boolean;
|
||||
google_entities: EntityFilter;
|
||||
@@ -187,3 +186,10 @@ export const updateCloudAlexaEntityConfig = (
|
||||
entity_id: entityId,
|
||||
...values,
|
||||
});
|
||||
|
||||
export interface CloudTTSInfo {
|
||||
languages: Array<[string, string]>;
|
||||
}
|
||||
|
||||
export const getCloudTTSInfo = (hass: HomeAssistant) =>
|
||||
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });
|
||||
|
@@ -1,70 +0,0 @@
|
||||
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]));
|
||||
};
|
@@ -1,24 +0,0 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
interface ValidConfig {
|
||||
valid: true;
|
||||
error: null;
|
||||
}
|
||||
|
||||
interface InvalidConfig {
|
||||
valid: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
type ValidKeys = "trigger" | "action" | "condition";
|
||||
|
||||
export const validateConfig = <
|
||||
T extends Partial<{ [key in ValidKeys]: unknown }>
|
||||
>(
|
||||
hass: HomeAssistant,
|
||||
config: T
|
||||
): Promise<Record<keyof T, ValidConfig | InvalidConfig>> =>
|
||||
hass.callWS({
|
||||
type: "validate_config",
|
||||
...config,
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user